diff --git a/lettings/tests/__init__.py b/lettings/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lettings/tests/conftest.py b/lettings/tests/conftest.py
new file mode 100644
index 0000000000..66ffe3eac4
--- /dev/null
+++ b/lettings/tests/conftest.py
@@ -0,0 +1,46 @@
+"""
+Fixture module for lettings tests
+"""
+import pytest
+
+from _pytest.monkeypatch import MonkeyPatch
+
+from lettings.models import Address, Letting
+
+
+@pytest.fixture(autouse=True)
+def disable_sentry(monkeypatch: MonkeyPatch):
+ monkeypatch.setattr(
+ "monitoring.sentry_sdk.init",
+ lambda *args, **kwargs: None
+ )
+
+
+@pytest.fixture
+def get_address():
+ """
+ Fixture that returns fictive address
+ Returns:
+ The address object
+ """
+ return Address.objects.create(
+ number=500,
+ street="Address test",
+ city="City test",
+ state="State test",
+ zip_code=99999,
+ country_iso_code="Country iso code test"
+ )
+
+
+@pytest.fixture
+def get_letting(get_address: Address):
+ """
+ Fixture that returns fictive letting
+ Returns:
+ The letting object
+ """
+ return Letting.objects.create(
+ title="Test Letting",
+ address=get_address
+ )
diff --git a/lettings/tests/tests.py b/lettings/tests/tests.py
new file mode 100644
index 0000000000..ae75dbcb90
--- /dev/null
+++ b/lettings/tests/tests.py
@@ -0,0 +1,130 @@
+"""
+Tests module for lettings app
+"""
+import pytest
+
+from django.urls import reverse, resolve
+from django.test import Client
+from pytest_django.asserts import assertTemplateUsed
+from _pytest.monkeypatch import MonkeyPatch
+
+from lettings.models import Letting, Address
+
+
+class TestLettingsUrl:
+ def test_lettings_index_url(self):
+ path = reverse(viewname="lettings:index")
+
+ assert path == "/lettings/"
+ assert resolve(path).view_name == "lettings:index"
+
+ @pytest.mark.django_db
+ def test_lettings_letting_url(self, get_address: Address, get_letting: Letting):
+ path = reverse(viewname="lettings:letting", kwargs={"letting_id": get_letting.id})
+
+ assert path == "/lettings/1/"
+ assert resolve(path).view_name == "lettings:letting"
+
+
+class TestLettingsView:
+ @pytest.mark.django_db
+ def test_lettings_index_view_ok(self, get_letting: Letting):
+ client = Client()
+ path = reverse(viewname="lettings:index")
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = ''
+ expected_content = f'
{get_letting.title}'
+
+ assert expected_h1 in content
+ assert expected_content in content
+ assert response.status_code == 200
+ assertTemplateUsed(response, template_name="lettings/index.html")
+
+ @pytest.mark.django_db
+ def test_lettings_index_view_returns_500(self, monkeypatch: MonkeyPatch, get_letting: Letting):
+ def raise_error():
+ raise Exception("forced error")
+
+ monkeypatch.setattr("lettings.views.Letting.objects.all", raise_error)
+
+ client = Client()
+ path = reverse(viewname="lettings:index")
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = (f'')
+
+ assert expected_h1 in content
+ assert response.status_code == 500
+ assertTemplateUsed(response, template_name="error_500.html")
+
+ @pytest.mark.django_db
+ def test_lettings_letting_view_ok(self, get_address: Address, get_letting: Letting):
+ client = Client()
+ path = reverse(viewname="lettings:letting", kwargs={"letting_id": get_letting.id})
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = f''
+ expected_content = (f'
{get_address.number} {get_address.street}
\n\t\t\t'
+ f'
{get_address.city}, {get_address.state} '
+ f'{get_address.zip_code}
\n\t\t\t'
+ f'
{get_address.country_iso_code}
')
+
+ assert expected_h1 in content
+ assert expected_content in content
+ assert response.status_code == 200
+ assertTemplateUsed(response, template_name="lettings/letting.html")
+
+ @pytest.mark.django_db
+ def test_lettings_letting_view_returns_404(self, get_address: Address, get_letting: Letting):
+ client = Client()
+ path = reverse(viewname="lettings:letting", kwargs={"letting_id": 2})
+
+ response = client.get(path=path)
+ content = response.content.decode()
+
+ expected_h1 = (f'')
+
+ assert expected_h1 in content
+ assert response.status_code == 404
+ assertTemplateUsed(response, template_name="error_404.html")
+
+ @pytest.mark.django_db
+ def test_lettings_letting_view_returns_500(self,
+ monkeypatch: MonkeyPatch,
+ get_letting: Letting):
+ def raise_error(*args, **kwargs):
+ raise Exception("forced error")
+
+ monkeypatch.setattr("lettings.views.Letting.objects.get", raise_error)
+
+ client = Client()
+ path = reverse(viewname="lettings:letting", kwargs={"letting_id": get_letting.id})
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = (f'')
+
+ assert expected_h1 in content
+ assert response.status_code == 500
+ assertTemplateUsed(response, template_name="error_500.html")
+
+
+class TestLettingsModel:
+ @pytest.mark.django_db
+ def test_lettings_address_model_ok(self, get_address: Address):
+ expected = f"{get_address.number} {get_address.street}"
+
+ assert str(get_address) == expected
+
+ @pytest.mark.django_db
+ def test_lettings_letting_model_ok(self, get_letting: Letting):
+ expected = f"{get_letting.title}"
+
+ assert str(get_letting) == expected
diff --git a/lettings/urls.py b/lettings/urls.py
new file mode 100644
index 0000000000..61a7ca5c89
--- /dev/null
+++ b/lettings/urls.py
@@ -0,0 +1,13 @@
+"""
+URLs module for lettings app
+"""
+from django.urls import path
+
+from . import views
+
+app_name = 'lettings'
+
+urlpatterns = [
+ path('', views.index, name='index'),
+ path('
/', views.letting, name='letting'),
+]
diff --git a/lettings/views.py b/lettings/views.py
new file mode 100644
index 0000000000..2536865100
--- /dev/null
+++ b/lettings/views.py
@@ -0,0 +1,86 @@
+"""
+Views module for lettings app
+"""
+from django.http import HttpRequest, HttpResponse
+from django.shortcuts import render
+
+from .models import Letting
+from monitoring import logger
+
+
+# Aenean leo magna, vestibulum et tincidunt fermentum, consectetur quis velit. Sed non placerat
+# massa. Integer est nunc, pulvinar a tempor et, bibendum id arcu. Vestibulum ante ipsum primis in
+# faucibus orci luctus et ultrices posuere cubilia curae; Cras eget scelerisque
+def index(request: HttpRequest) -> HttpResponse:
+ """
+ View function for lettings index page
+ Args:
+ request (HttpRequest): Http Request object
+
+ Returns:
+ An HTTP response with the list of lettings or an HTTP response with 500 error.
+ """
+ try:
+ lettings_list = Letting.objects.all()
+ context = {'lettings_list': lettings_list}
+
+ logger.info(f"Going to lettings index page : {context=}, status = 200.")
+
+ return render(request, template_name='lettings/index.html', context=context, status=200)
+
+ except Exception as e:
+ context = {"error": str(e)}
+
+ logger.error(f"Error 500 returned while reaching lettings index page : {context=},"
+ f" status = 500.")
+
+ return render(request, template_name='error_500.html', context=context, status=500)
+
+
+# Cras ultricies dignissim purus, vitae hendrerit ex varius non. In accumsan porta nisl id
+# eleifend. Praesent dignissim, odio eu consequat pretium, purus urna vulputate arcu, vitae
+# efficitur lacus justo nec purus. Aenean finibus faucibus lectus at porta. Maecenas auctor, est ut
+# luctus congue, dui enim mattis enim, ac condimentum velit libero in magna. Suspendisse potenti.
+# In tempus a nisi sed laoreet. Suspendisse porta dui eget sem accumsan interdum. Ut quis urna
+# pellentesque justo mattis ullamcorper ac non tellus. In tristique mauris eu velit fermentum,
+# tempus pharetra est luctus. Vivamus consequat aliquam libero, eget bibendum lorem. Sed non dolor
+# risus. Mauris condimentum auctor elementum. Donec quis nisi ligula. Integer vehicula tincidunt
+# enim, ac lacinia augue pulvinar sit amet.
+def letting(request: HttpRequest, letting_id: int) -> HttpResponse:
+ """
+ View function for letting detail page
+ Args:
+ request (HttpRequest): Http Request object
+ letting_id (int): letting id
+
+ Returns:
+ An HTTP response with the letting detail or an HTTP response with 404 error if not found
+ or an HTTP response with 500 error
+ """
+ try:
+ letting = Letting.objects.get(id=letting_id)
+
+ context = {
+ 'title': letting.title,
+ 'address': letting.address,
+ }
+
+ logger.info(f"Going to lettings details page : {context=}, status = 200.")
+
+ return render(request, template_name='lettings/letting.html', context=context, status=200)
+
+ except Letting.DoesNotExist as e:
+ context = {"type": "letting", "id": letting_id, "error": str(e)}
+
+ logger.warning(f"Error 404 returned while reaching letting n°{letting_id} : {context=},"
+ f" status = 404.")
+
+ return render(request, template_name='error_404.html', context=context, status=404)
+
+ except Exception as e:
+ context = {"error": str(e)}
+
+ logger.error(f"Error 500 returned while reaching letting details page : {context=},"
+ f" status = 500.")
+
+ return render(request, template_name='error_500.html', context=context, status=500)
diff --git a/manage.py b/manage.py
index c0e27e034a..961c945a68 100755
--- a/manage.py
+++ b/manage.py
@@ -1,3 +1,6 @@
+"""
+Main module
+"""
import os
import sys
diff --git a/monitoring.py b/monitoring.py
new file mode 100644
index 0000000000..309dd0ca46
--- /dev/null
+++ b/monitoring.py
@@ -0,0 +1,35 @@
+"""
+Module for monitoring with Sentry
+"""
+import logging
+import sentry_sdk
+
+from sentry_sdk.integrations.logging import LoggingIntegration
+
+from config import SENTRY_KEY
+
+logger = logging.getLogger(__name__)
+
+
+def init_sentry():
+ """
+ Method to initialize sentry integration
+ """
+ logging.basicConfig(level=logging.INFO)
+
+ sentry_logging = LoggingIntegration(
+ level=logging.INFO,
+ event_level=logging.ERROR,
+ )
+
+ sentry_sdk.init(
+ dsn=SENTRY_KEY,
+ # Add request headers and IP for users,
+ # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
+ send_default_pii=True,
+
+ # Enable logs to be sent to Sentry
+ enable_logs=True,
+
+ integrations=[sentry_logging]
+ )
diff --git a/oc-lettings-site.sqlite3 b/oc-lettings-site.sqlite3
index 3d885414f9..92f150a14e 100644
Binary files a/oc-lettings-site.sqlite3 and b/oc-lettings-site.sqlite3 differ
diff --git a/oc_lettings_site/admin.py b/oc_lettings_site/admin.py
index 63328c6dd3..e69de29bb2 100644
--- a/oc_lettings_site/admin.py
+++ b/oc_lettings_site/admin.py
@@ -1,10 +0,0 @@
-from django.contrib import admin
-
-from .models import Letting
-from .models import Address
-from .models import Profile
-
-
-admin.site.register(Letting)
-admin.site.register(Address)
-admin.site.register(Profile)
diff --git a/oc_lettings_site/apps.py b/oc_lettings_site/apps.py
index 6489692f04..805a860bb6 100644
--- a/oc_lettings_site/apps.py
+++ b/oc_lettings_site/apps.py
@@ -1,5 +1,13 @@
+"""
+App config module for oc_lettings_site app
+"""
from django.apps import AppConfig
class OCLettingsSiteConfig(AppConfig):
+ """
+ Namespace class for oc_lettings_site app
+ Attributes:
+ name (str): Name of the app
+ """
name = 'oc_lettings_site'
diff --git a/oc_lettings_site/migrations/0002_auto_20260518_1551.py b/oc_lettings_site/migrations/0002_auto_20260518_1551.py
new file mode 100644
index 0000000000..586b4a1db8
--- /dev/null
+++ b/oc_lettings_site/migrations/0002_auto_20260518_1551.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.0 on 2026-05-18 13:51
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('oc_lettings_site', '0001_initial'),
+ ('lettings', '0003_auto_20260518_1551'),
+ ('profiles', '0003_auto_20260518_1551'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='letting',
+ name='address',
+ ),
+ migrations.RemoveField(
+ model_name='profile',
+ name='user',
+ ),
+ migrations.DeleteModel(
+ name='Address',
+ ),
+ migrations.DeleteModel(
+ name='Letting',
+ ),
+ migrations.DeleteModel(
+ name='Profile',
+ ),
+ ]
diff --git a/oc_lettings_site/models.py b/oc_lettings_site/models.py
deleted file mode 100644
index ed255e8c11..0000000000
--- a/oc_lettings_site/models.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.db import models
-from django.core.validators import MaxValueValidator, MinLengthValidator
-from django.contrib.auth.models import User
-
-
-class Address(models.Model):
- number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)])
- street = models.CharField(max_length=64)
- city = models.CharField(max_length=64)
- state = models.CharField(max_length=2, validators=[MinLengthValidator(2)])
- zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)])
- country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)])
-
- def __str__(self):
- return f'{self.number} {self.street}'
-
-
-class Letting(models.Model):
- title = models.CharField(max_length=256)
- address = models.OneToOneField(Address, on_delete=models.CASCADE)
-
- def __str__(self):
- return self.title
-
-
-class Profile(models.Model):
- user = models.OneToOneField(User, on_delete=models.CASCADE)
- favorite_city = models.CharField(max_length=64, blank=True)
-
- def __str__(self):
- return self.user.username
diff --git a/oc_lettings_site/settings.py b/oc_lettings_site/settings.py
index a18bee8106..d2e781dc2c 100644
--- a/oc_lettings_site/settings.py
+++ b/oc_lettings_site/settings.py
@@ -1,3 +1,6 @@
+"""
+Settings module for oc_lettings_site app
+"""
import os
from pathlib import Path
@@ -22,6 +25,8 @@
INSTALLED_APPS = [
'oc_lettings_site.apps.OCLettingsSiteConfig',
+ 'lettings.apps.LettingsConfig',
+ 'profiles.apps.ProfilesConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -111,4 +116,4 @@
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'
-STATICFILES_DIRS = [BASE_DIR / "static",]
+STATICFILES_DIRS = [BASE_DIR / "static", ]
diff --git a/oc_lettings_site/tests.py b/oc_lettings_site/tests.py
deleted file mode 100644
index 3fd62bb718..0000000000
--- a/oc_lettings_site/tests.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def test_dummy():
- assert 1
diff --git a/oc_lettings_site/tests/__init__.py b/oc_lettings_site/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/oc_lettings_site/tests/conftest.py b/oc_lettings_site/tests/conftest.py
new file mode 100644
index 0000000000..943f1fbb0b
--- /dev/null
+++ b/oc_lettings_site/tests/conftest.py
@@ -0,0 +1,14 @@
+"""
+Fixture module for oc_lettings_site tests
+"""
+import pytest
+
+from _pytest.monkeypatch import MonkeyPatch
+
+
+@pytest.fixture(autouse=True)
+def disable_sentry(monkeypatch: MonkeyPatch):
+ monkeypatch.setattr(
+ "monitoring.sentry_sdk.init",
+ lambda *args, **kwargs: None
+ )
diff --git a/oc_lettings_site/tests/tests.py b/oc_lettings_site/tests/tests.py
new file mode 100644
index 0000000000..1dcc30af82
--- /dev/null
+++ b/oc_lettings_site/tests/tests.py
@@ -0,0 +1,55 @@
+"""
+Tests module for oc_lettings_site app
+"""
+import pytest
+
+from django.template.response import TemplateResponse
+from django.test import Client
+from django.urls import reverse, resolve
+from pytest_django.asserts import assertTemplateUsed
+from _pytest.monkeypatch import MonkeyPatch
+
+
+class TestOcLettingsSiteUrl:
+ def test_index_url(self):
+ path = reverse("index")
+
+ assert path == "/"
+ assert resolve(path).view_name == 'index'
+
+
+class TestOcLettingsSiteView:
+ @pytest.mark.django_db
+ def test_oc_lettings_site_index_view_ok(self):
+ client = Client()
+ path = reverse(viewname="index")
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = (f'')
+
+ assert expected_h1 in content
+ assert response.status_code == 200
+ assertTemplateUsed(response, template_name="index.html")
+
+ @pytest.mark.django_db
+ def test_oc_lettings_site_index_view_returns_500(self, monkeypatch: MonkeyPatch):
+ def side_effect(request, template_name, context=None, status=500):
+ if template_name == "index.html":
+ raise Exception("forced error")
+ return TemplateResponse(request, template_name, context or {}, status=status)
+
+ monkeypatch.setattr("oc_lettings_site.views.render", side_effect)
+
+ client = Client()
+ path = reverse(viewname="index")
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = (f'')
+
+ assert expected_h1 in content
+ assert response.status_code == 500
+ assertTemplateUsed(response, template_name="error_500.html")
diff --git a/oc_lettings_site/urls.py b/oc_lettings_site/urls.py
index f0ff5897ab..87eebf52d6 100644
--- a/oc_lettings_site/urls.py
+++ b/oc_lettings_site/urls.py
@@ -1,13 +1,15 @@
+"""
+URLs module for oc_lettings_site app.
+Include the lettings and profiles apps urls
+"""
from django.contrib import admin
-from django.urls import path
+from django.urls import path, include
from . import views
urlpatterns = [
path('', views.index, name='index'),
- path('lettings/', views.lettings_index, name='lettings_index'),
- path('lettings//', views.letting, name='letting'),
- path('profiles/', views.profiles_index, name='profiles_index'),
- path('profiles//', views.profile, name='profile'),
+ path('lettings/', include('lettings.urls')),
+ path('profiles/', include('profiles.urls')),
path('admin/', admin.site.urls),
]
diff --git a/oc_lettings_site/views.py b/oc_lettings_site/views.py
index a72db27074..12b562c384 100644
--- a/oc_lettings_site/views.py
+++ b/oc_lettings_site/views.py
@@ -1,45 +1,37 @@
+"""
+Views module for oc_lettings_site app
+"""
+from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
-from .models import Letting, Profile
-
-
-
-
-# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque molestie quam lobortis leo consectetur ullamcorper non id est. Praesent dictum, nulla eget feugiat sagittis, sem mi convallis eros,
-# vitae dapibus nisi lorem dapibus sem. Maecenas pharetra purus ipsum, eget consequat ipsum lobortis quis. Phasellus eleifend ex auctor venenatis tempus.
-# Aliquam vitae erat ac orci placerat luctus. Nullam elementum urna nisi, pellentesque iaculis enim cursus in. Praesent volutpat porttitor magna, non finibus neque cursus id.
-def index(request):
- return render(request, 'index.html')
-
-# Aenean leo magna, vestibulum et tincidunt fermentum, consectetur quis velit. Sed non placerat massa. Integer est nunc, pulvinar a
-# tempor et, bibendum id arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Cras eget scelerisque
-def lettings_index(request):
- lettings_list = Letting.objects.all()
- context = {'lettings_list': lettings_list}
- return render(request, 'lettings_index.html', context)
-
-
-#Cras ultricies dignissim purus, vitae hendrerit ex varius non. In accumsan porta nisl id eleifend. Praesent dignissim, odio eu consequat pretium, purus urna vulputate arcu, vitae efficitur
-# lacus justo nec purus. Aenean finibus faucibus lectus at porta. Maecenas auctor, est ut luctus congue, dui enim mattis enim, ac condimentum velit libero in magna. Suspendisse potenti. In tempus a nisi sed laoreet.
-# Suspendisse porta dui eget sem accumsan interdum. Ut quis urna pellentesque justo mattis ullamcorper ac non tellus. In tristique mauris eu velit fermentum, tempus pharetra est luctus. Vivamus consequat aliquam libero, eget bibendum lorem. Sed non dolor risus. Mauris condimentum auctor elementum. Donec quis nisi ligula. Integer vehicula tincidunt enim, ac lacinia augue pulvinar sit amet.
-def letting(request, letting_id):
- letting = Letting.objects.get(id=letting_id)
- context = {
- 'title': letting.title,
- 'address': letting.address,
- }
- return render(request, 'letting.html', context)
-
-# Sed placerat quam in pulvinar commodo. Nullam laoreet consectetur ex, sed consequat libero pulvinar eget. Fusc
-# faucibus, urna quis auctor pharetra, massa dolor cursus neque, quis dictum lacus d
-def profiles_index(request):
- profiles_list = Profile.objects.all()
- context = {'profiles_list': profiles_list}
- return render(request, 'profiles_index.html', context)
-
-# Aliquam sed metus eget nisi tincidunt ornare accumsan eget lac
-# laoreet neque quis, pellentesque dui. Nullam facilisis pharetra vulputate. Sed tincidunt, dolor id facilisis fringilla, eros leo tristique lacus,
-# it. Nam aliquam dignissim congue. Pellentesque habitant morbi tristique senectus et netus et males
-def profile(request, username):
- profile = Profile.objects.get(user__username=username)
- context = {'profile': profile}
- return render(request, 'profile.html', context)
+
+from monitoring import init_sentry, logger
+
+
+# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque molestie quam lobortis leo
+# consectetur ullamcorper non id est. Praesent dictum, nulla eget feugiat sagittis, sem mi
+# convallis eros, vitae dapibus nisi lorem dapibus sem. Maecenas pharetra purus ipsum, eget
+# consequat ipsum lobortis quis. Phasellus eleifend ex auctor venenatis tempus. Aliquam vitae erat
+# ac orci placerat luctus. Nullam elementum urna nisi, pellentesque iaculis enim cursus in.
+# Praesent volutpat porttitor magna, non finibus neque cursus id.
+def index(request: HttpRequest) -> HttpResponse:
+ """
+ View function for home page
+ Args:
+ request (HttpRequest): Http Request object
+
+ Returns:
+ An HTTP response with index page or HTTP response with 500 error.
+ """
+ init_sentry()
+ try:
+ logger.info(f"Going to home page : status = 200.")
+
+ return render(request, template_name='index.html')
+
+ except Exception as e:
+ context = {'error': str(e)}
+
+ logger.error(f"Error 500 returned while reaching home page : {context=}"
+ f", status = 500.")
+
+ return render(request, template_name='error_500.html', context=context)
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000000..222502339e
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,733 @@
+# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand.
+
+[[package]]
+name = "asgiref"
+version = "3.11.1"
+description = "ASGI specs, helper code, and adapters"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"},
+ {file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"},
+]
+
+[package.dependencies]
+typing_extensions = {version = ">=4", markers = "python_version < \"3.11\""}
+
+[package.extras]
+tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"]
+
+[[package]]
+name = "certifi"
+version = "2026.4.22"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"},
+ {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"},
+]
+
+[[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"
+groups = ["main"]
+markers = "sys_platform == \"win32\""
+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.14.0"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075"},
+ {file = "coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85"},
+ {file = "coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323"},
+ {file = "coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a"},
+ {file = "coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480"},
+ {file = "coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c"},
+ {file = "coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3"},
+ {file = "coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1"},
+ {file = "coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627"},
+ {file = "coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5"},
+ {file = "coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595"},
+ {file = "coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27"},
+ {file = "coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2"},
+ {file = "coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d"},
+ {file = "coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef"},
+ {file = "coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe"},
+ {file = "coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae"},
+ {file = "coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e"},
+ {file = "coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96"},
+ {file = "coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90"},
+ {file = "coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477"},
+ {file = "coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab"},
+ {file = "coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917"},
+ {file = "coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8"},
+ {file = "coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d"},
+ {file = "coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca"},
+ {file = "coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828"},
+ {file = "coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d"},
+ {file = "coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9"},
+ {file = "coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1"},
+ {file = "coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db"},
+ {file = "coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2"},
+ {file = "coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644"},
+ {file = "coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b"},
+ {file = "coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1"},
+ {file = "coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74"},
+]
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
+
+[[package]]
+name = "django"
+version = "3.0"
+description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
+optional = false
+python-versions = ">=3.6"
+groups = ["main"]
+files = [
+ {file = "Django-3.0-py3-none-any.whl", hash = "sha256:6f857bd4e574442ba35a7172f1397b303167dae964cf18e53db5e85fe248d000"},
+ {file = "Django-3.0.tar.gz", hash = "sha256:d98c9b6e5eed147bc51f47c014ff6826bd1ab50b166956776ee13db5a58804ae"},
+]
+
+[package.dependencies]
+asgiref = ">=3.2,<4.0"
+pytz = "*"
+sqlparse = ">=0.2.2"
+
+[package.extras]
+argon2 = ["argon2-cffi (>=16.1.0)"]
+bcrypt = ["bcrypt"]
+
+[[package]]
+name = "entrypoints"
+version = "0.3"
+description = "Discover and load entry points from installed packages."
+optional = false
+python-versions = ">=2.7"
+groups = ["main"]
+files = [
+ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"},
+ {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+markers = "python_version == \"3.10\""
+files = [
+ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"},
+ {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""}
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "flake8"
+version = "3.7.0"
+description = "the modular source code checker: pep8, pyflakes and co"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+groups = ["main"]
+files = [
+ {file = "flake8-3.7.0-py2.py3-none-any.whl", hash = "sha256:7eda8e5c29ac9e0d3c0fd44649298c29768efc79a9b872b41c8f05ca5f502c83"},
+ {file = "flake8-3.7.0.tar.gz", hash = "sha256:2baac1c277d917f3e01ce17a60dcfad4a4ce13a2c5d50a15d0811302a3bdf7aa"},
+]
+
+[package.dependencies]
+entrypoints = ">=0.3.0,<0.4.0"
+mccabe = ">=0.6.0,<0.7.0"
+pycodestyle = ">=2.5.0,<2.6.0"
+pyflakes = ">=2.1.0,<2.2.0"
+
+[[package]]
+name = "flake8-html"
+version = "0.4.3"
+description = "Generate HTML reports of flake8 violations"
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "flake8-html-0.4.3.tar.gz", hash = "sha256:8b870299620cc4a06f73644a1b4d457799abeca1cc914c62ae71ec5bf65c79a5"},
+ {file = "flake8_html-0.4.3-py2.py3-none-any.whl", hash = "sha256:8f126748b1b0edd6cd39e87c6192df56e2f8655b0aa2bb00ffeac8cf27be4325"},
+]
+
+[package.dependencies]
+flake8 = ">=3.3.0"
+jinja2 = ">=3.1.0"
+pygments = ">=2.2.0"
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
+ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
+ {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
+ {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"},
+ {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"},
+ {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"},
+ {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"},
+ {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"},
+ {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"},
+ {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"},
+ {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
+ {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"},
+ {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"},
+ {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"},
+ {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
+]
+
+[[package]]
+name = "mccabe"
+version = "0.6.1"
+description = "McCabe checker, plugin for flake8"
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
+ {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
+ {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["coverage", "pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pycodestyle"
+version = "2.5.0"
+description = "Python style guide checker"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+groups = ["main"]
+files = [
+ {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"},
+ {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"},
+]
+
+[[package]]
+name = "pyflakes"
+version = "2.1.1"
+description = "passive checker of Python programs"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+groups = ["main"]
+files = [
+ {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"},
+ {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"},
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
+ {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pytest"
+version = "9.0.3"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
+ {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""}
+iniconfig = ">=1.0.1"
+packaging = ">=22"
+pluggy = ">=1.5,<2"
+pygments = ">=2.7.2"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "7.1.0"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"},
+ {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"},
+]
+
+[package.dependencies]
+coverage = {version = ">=7.10.6", extras = ["toml"]}
+pluggy = ">=1.2"
+pytest = ">=7"
+
+[package.extras]
+testing = ["process-tests", "pytest-xdist", "virtualenv"]
+
+[[package]]
+name = "pytest-django"
+version = "4.12.0"
+description = "A Django plugin for pytest."
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85"},
+ {file = "pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0"
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.2"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
+ {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
+]
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
+[[package]]
+name = "pytz"
+version = "2026.2"
+description = "World timezone definitions, modern and historical"
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126"},
+ {file = "pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a"},
+]
+
+[[package]]
+name = "sentry-sdk"
+version = "2.60.0"
+description = "Python client for Sentry (https://sentry.io)"
+optional = false
+python-versions = ">=3.6"
+groups = ["main"]
+files = [
+ {file = "sentry_sdk-2.60.0-py3-none-any.whl", hash = "sha256:28a536c03291c8bcb363cf35c611b32738ec118ff64d8d6383b096448ac4c803"},
+ {file = "sentry_sdk-2.60.0.tar.gz", hash = "sha256:0bd25e54e78ca02d0be512529fa644bbbf9e8470d7b26371294012d4ca93c978"},
+]
+
+[package.dependencies]
+certifi = "*"
+urllib3 = ">=1.26.11"
+
+[package.extras]
+aiohttp = ["aiohttp (>=3.5)"]
+anthropic = ["anthropic (>=0.16)"]
+arq = ["arq (>=0.23)"]
+asyncio = ["httpcore[asyncio] (==1.*)"]
+asyncpg = ["asyncpg (>=0.23)"]
+beam = ["apache-beam (>=2.12)"]
+bottle = ["bottle (>=0.12.13)"]
+celery = ["celery (>=3)"]
+celery-redbeat = ["celery-redbeat (>=2)"]
+chalice = ["chalice (>=1.16.0)"]
+clickhouse-driver = ["clickhouse-driver (>=0.2.0)"]
+django = ["django (>=1.8)"]
+falcon = ["falcon (>=1.4)"]
+fastapi = ["fastapi (>=0.79.0)"]
+flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
+google-genai = ["google-genai (>=1.29.0)"]
+grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"]
+http2 = ["httpcore[http2] (==1.*)"]
+httpx = ["httpx (>=0.16.0)"]
+huey = ["huey (>=2)"]
+huggingface-hub = ["huggingface_hub (>=0.22)"]
+langchain = ["langchain (>=0.0.210)"]
+langgraph = ["langgraph (>=0.6.6)"]
+launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"]
+litellm = ["litellm (>=1.77.5,!=1.82.7,!=1.82.8)"]
+litestar = ["litestar (>=2.0.0)"]
+loguru = ["loguru (>=0.5)"]
+mcp = ["mcp (>=1.15.0)"]
+openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"]
+openfeature = ["openfeature-sdk (>=0.7.1)"]
+opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
+opentelemetry-experimental = ["opentelemetry-distro"]
+opentelemetry-otlp = ["opentelemetry-distro[otlp] (>=0.35b0)"]
+pure-eval = ["asttokens", "executing", "pure_eval"]
+pydantic-ai = ["pydantic-ai (>=1.0.0)"]
+pymongo = ["pymongo (>=3.1)"]
+pyspark = ["pyspark (>=2.4.4)"]
+quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
+rq = ["rq (>=0.6)"]
+sanic = ["sanic (>=0.8)"]
+sqlalchemy = ["sqlalchemy (>=1.2)"]
+starlette = ["starlette (>=0.19.1)"]
+starlite = ["starlite (>=1.48)"]
+statsig = ["statsig (>=0.55.3)"]
+tornado = ["tornado (>=6)"]
+unleash = ["UnleashClient (>=6.0.1)"]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["main"]
+files = [
+ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
+ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
+]
+
+[[package]]
+name = "sqlparse"
+version = "0.5.5"
+description = "A non-validating SQL parser."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"},
+ {file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"},
+]
+
+[package.extras]
+dev = ["build"]
+doc = ["sphinx"]
+
+[[package]]
+name = "tomli"
+version = "2.4.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+markers = "python_full_version <= \"3.11.0a6\""
+files = [
+ {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"},
+ {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"},
+ {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"},
+ {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"},
+ {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"},
+ {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"},
+ {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"},
+ {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"},
+ {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"},
+ {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"},
+ {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"},
+ {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"},
+ {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"},
+ {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"},
+ {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"},
+ {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"},
+ {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"},
+ {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"},
+ {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"},
+ {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"},
+ {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"},
+ {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"},
+ {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"},
+ {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"},
+ {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"},
+ {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"},
+ {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"},
+ {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"},
+ {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"},
+ {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"},
+ {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"},
+ {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"},
+ {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"},
+ {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"},
+ {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"},
+ {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"},
+ {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"},
+ {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"},
+ {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"},
+ {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"},
+ {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"},
+ {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"},
+ {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"},
+ {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"},
+ {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"},
+ {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"},
+ {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+description = "Backported and Experimental Type Hints for Python 3.9+"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+markers = "python_version == \"3.10\""
+files = [
+ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
+ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
+]
+
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
+ {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
+
+[metadata]
+lock-version = "2.1"
+python-versions = ">=3.10"
+content-hash = "6e2f2adfbc0ae06398494a726625a155716005abb006e86c1ed008ddd97cbbab"
diff --git a/profiles/__init__.py b/profiles/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/profiles/admin.py b/profiles/admin.py
new file mode 100644
index 0000000000..2b417bb0da
--- /dev/null
+++ b/profiles/admin.py
@@ -0,0 +1,9 @@
+"""
+Admin registration module for profiles app
+"""
+from django.contrib import admin
+
+from profiles.models import Profile
+
+
+admin.site.register(Profile)
diff --git a/profiles/apps.py b/profiles/apps.py
new file mode 100644
index 0000000000..b0c6010f11
--- /dev/null
+++ b/profiles/apps.py
@@ -0,0 +1,13 @@
+"""
+Namespace module for profiles app
+"""
+from django.apps import AppConfig
+
+
+class ProfilesConfig(AppConfig):
+ """
+ Namespace class for profiles app
+ Attributes:
+ name: namespace of the profiles app
+ """
+ name = 'profiles'
diff --git a/profiles/migrations/0001_initial.py b/profiles/migrations/0001_initial.py
new file mode 100644
index 0000000000..069a3babcc
--- /dev/null
+++ b/profiles/migrations/0001_initial.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.0 on 2026-05-18 12:53
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Profile',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('favorite_city', models.CharField(blank=True, max_length=64)),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='new_profile', to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/profiles/migrations/0002_auto_20260518_1548.py b/profiles/migrations/0002_auto_20260518_1548.py
new file mode 100644
index 0000000000..7be66c52bf
--- /dev/null
+++ b/profiles/migrations/0002_auto_20260518_1548.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.0 on 2026-05-18 13:48
+
+from django.db import migrations
+
+
+def copy_profile(apps, schema_editor):
+ OldProfile = apps.get_model('oc_lettings_site', 'Profile')
+ NewProfile = apps.get_model('profiles', 'Profile')
+
+ for obj in OldProfile.objects.all():
+ NewProfile.objects.create(
+ id=obj.id,
+ user=obj.user,
+ favorite_city=obj.favorite_city,
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('profiles', '0001_initial'),
+ ('oc_lettings_site', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunPython(copy_profile),
+ ]
diff --git a/profiles/migrations/0003_auto_20260518_1551.py b/profiles/migrations/0003_auto_20260518_1551.py
new file mode 100644
index 0000000000..61fbe40024
--- /dev/null
+++ b/profiles/migrations/0003_auto_20260518_1551.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.0 on 2026-05-18 13:51
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('profiles', '0002_auto_20260518_1548'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='profile',
+ name='user',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/profiles/migrations/__init__.py b/profiles/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/profiles/models.py b/profiles/models.py
new file mode 100644
index 0000000000..5893b3c972
--- /dev/null
+++ b/profiles/models.py
@@ -0,0 +1,24 @@
+"""
+Models module for profiles app
+"""
+from django.db import models
+from django.contrib.auth.models import User
+
+
+class Profile(models.Model):
+ """
+ Models class for profiles app
+ Attributes:
+ user (User): user
+ favorite_city (str): The favorite city of the user
+ """
+ user = models.OneToOneField(User, on_delete=models.CASCADE)
+ favorite_city = models.CharField(max_length=64, blank=True)
+
+ def __str__(self) -> str:
+ """
+ String method for profile model
+ Returns:
+ The user name of the profile
+ """
+ return self.user.username
diff --git a/templates/profiles_index.html b/profiles/templates/profiles/index.html
similarity index 88%
rename from templates/profiles_index.html
rename to profiles/templates/profiles/index.html
index 4ad1daf92f..563b7a0166 100644
--- a/templates/profiles_index.html
+++ b/profiles/templates/profiles/index.html
@@ -18,7 +18,7 @@
@@ -34,7 +34,7 @@
Home
-
+
Lettings
diff --git a/templates/profile.html b/profiles/templates/profiles/profile.html
similarity index 96%
rename from templates/profile.html
rename to profiles/templates/profiles/profile.html
index d150d30e63..4b1af37496 100644
--- a/templates/profile.html
+++ b/profiles/templates/profiles/profile.html
@@ -24,14 +24,14 @@
diff --git a/profiles/tests/__init__.py b/profiles/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/profiles/tests/conftest.py b/profiles/tests/conftest.py
new file mode 100644
index 0000000000..f6addc0755
--- /dev/null
+++ b/profiles/tests/conftest.py
@@ -0,0 +1,34 @@
+"""
+Fixture module for profiles tests
+"""
+import pytest
+
+from _pytest.monkeypatch import MonkeyPatch
+from django.contrib.auth.models import User
+
+from profiles.models import Profile
+
+
+@pytest.fixture(autouse=True)
+def disable_sentry(monkeypatch: MonkeyPatch):
+ monkeypatch.setattr(
+ "monitoring.sentry_sdk.init",
+ lambda *args, **kwargs: None
+ )
+
+
+@pytest.fixture
+def get_profile():
+ """
+ Fixture that returns fictive profile
+ Returns:
+ The profile object
+ """
+ return Profile.objects.create(
+ user=User.objects.create_user(username='Username',
+ first_name='First',
+ last_name='Last',
+ email='test@test.com',
+ password='test_pwd'),
+ favorite_city="City Test"
+ )
diff --git a/profiles/tests/tests.py b/profiles/tests/tests.py
new file mode 100644
index 0000000000..dc7cb0d955
--- /dev/null
+++ b/profiles/tests/tests.py
@@ -0,0 +1,135 @@
+"""
+Tests module for profiles app
+"""
+import pytest
+
+from django.contrib.auth.models import User
+from django.urls import reverse, resolve
+from django.test import Client
+from pytest_django.asserts import assertTemplateUsed
+from _pytest.monkeypatch import MonkeyPatch
+
+from profiles.models import Profile
+
+
+class TestProfilesUrl:
+ def test_profiles_index_url(self):
+ path = reverse("profiles:index")
+
+ assert path == "/profiles/"
+ assert resolve(path).view_name == 'profiles:index'
+
+ @pytest.mark.django_db
+ def test_profiles_profile_url(self):
+ Profile.objects.create(user=User.objects.create_user(username="Username",
+ first_name='First Name',
+ last_name='Last Name',
+ email='test@test.com'),
+ favorite_city="Paris")
+
+ path = reverse(viewname="profiles:profile", kwargs={'username': "Username"})
+
+ assert path == "/profiles/Username/"
+ assert resolve(path).view_name == "profiles:profile"
+
+
+class TestProfilesView:
+ @pytest.mark.django_db
+ def test_profiles_index_view_ok(self, get_profile: Profile):
+ client = Client()
+ path = reverse(viewname="profiles:index")
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = ''
+ expected_content = (f'
'
+ f'{get_profile.user.username}')
+
+ assert expected_h1 in content
+ assert expected_content in content
+ assert response.status_code == 200
+ assertTemplateUsed(response, template_name="profiles/index.html")
+
+ @pytest.mark.django_db
+ def test_profiles_index_view_returns_500(self, monkeypatch: MonkeyPatch, get_profile: Profile):
+ def raise_error():
+ raise Exception("forced error")
+
+ monkeypatch.setattr("profiles.views.Profile.objects.all", raise_error)
+
+ client = Client()
+ path = reverse(viewname="profiles:index")
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = (f'')
+
+ assert expected_h1 in content
+ assert response.status_code == 500
+ assertTemplateUsed(response, template_name="error_500.html")
+
+ @pytest.mark.django_db
+ def test_profiles_profile_view_ok(self, get_profile: Profile):
+ client = Client()
+ path = reverse(viewname="profiles:profile", kwargs={"username": get_profile.user.username})
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = (f'')
+ expected_content = (f'
First name : {get_profile.user.first_name}'
+ f'
\n\t\t\t'
+ f'
Last name : {get_profile.user.last_name}'
+ f'
\n\t\t\t'
+ f'
Email : {get_profile.user.email}
\n\t\t\t'
+ f'
Favorite city : {get_profile.favorite_city}
')
+
+ assert expected_h1 in content
+ assert expected_content in content
+ assert response.status_code == 200
+ assertTemplateUsed(response, template_name="profiles/profile.html")
+
+ @pytest.mark.django_db
+ def test_profiles_profile_view_returns_404(self, get_profile: Profile):
+ client = Client()
+ path = reverse(viewname="profiles:profile", kwargs={"username": "test"})
+
+ response = client.get(path=path)
+ content = response.content.decode()
+
+ expected_h1 = (f'')
+
+ assert expected_h1 in content
+ assert response.status_code == 404
+ assertTemplateUsed(response, template_name="error_404.html")
+
+ @pytest.mark.django_db
+ def test_profiles_profile_view_returns_500(self,
+ monkeypatch: MonkeyPatch,
+ get_profile: Profile):
+ def raise_error(*args, **kwargs):
+ raise Exception("forced error")
+
+ monkeypatch.setattr("profiles.views.Profile.objects.get", raise_error)
+
+ client = Client()
+ path = reverse(viewname="profiles:profile", kwargs={"username": get_profile.user.username})
+
+ response = client.get(path=path)
+ content = response.content.decode()
+ expected_h1 = (f'')
+
+ assert expected_h1 in content
+ assert response.status_code == 500
+ assertTemplateUsed(response, template_name="error_500.html")
+
+
+class TestProfilesModel:
+ @pytest.mark.django_db
+ def test_profiles_profile_model_ok(self, get_profile: Profile):
+ expected = f"{get_profile.user.username}"
+
+ assert str(get_profile) == expected
diff --git a/profiles/urls.py b/profiles/urls.py
new file mode 100644
index 0000000000..1efb5f1a21
--- /dev/null
+++ b/profiles/urls.py
@@ -0,0 +1,13 @@
+"""
+URLs module for profiles app
+"""
+from django.urls import path
+
+from . import views
+
+app_name = 'profiles'
+
+urlpatterns = [
+ path('', views.index, name='index'),
+ path('
/', views.profile, name='profile'),
+]
diff --git a/profiles/views.py b/profiles/views.py
new file mode 100644
index 0000000000..e6b3d808f0
--- /dev/null
+++ b/profiles/views.py
@@ -0,0 +1,77 @@
+"""
+Views module for profiles app
+"""
+from django.http import HttpRequest, HttpResponse
+from django.shortcuts import render
+from monitoring import logger
+
+from profiles.models import Profile
+
+
+# Sed placerat quam in pulvinar commodo. Nullam laoreet consectetur ex, sed consequat libero
+# pulvinar eget. Fusc faucibus, urna quis auctor pharetra, massa dolor cursus neque, quis dictum
+# lacus d
+def index(request: HttpRequest) -> HttpResponse:
+ """
+ View function for profiles index page
+ Args:
+ request (HttpRequest): request object
+
+ Returns:
+ An HTTP response with the list of profiles or HTTP response with 500 error.
+ """
+ try:
+ profiles_list = Profile.objects.all()
+ context = {'profiles_list': profiles_list}
+
+ logger.info(f"Going to profiles index page : {context=}, status = 200.")
+
+ return render(request, template_name='profiles/index.html', context=context, status=200)
+
+ except Exception as e:
+ context = {"error": str(e)}
+
+ logger.error(f"Error 500 returned while reaching profiles index page : {context=}"
+ f", status = 500.")
+
+ return render(request, template_name='error_500.html', context=context, status=500)
+
+
+# Aliquam sed metus eget nisi tincidunt ornare accumsan eget lac
+# laoreet neque quis, pellentesque dui. Nullam facilisis pharetra vulputate. Sed tincidunt, dolor
+# id facilisis fringilla, eros leo tristique lacus, it. Nam aliquam dignissim congue. Pellentesque
+# habitant morbi tristique senectus et netus et males
+def profile(request: HttpRequest, username: str):
+ """
+ View function for profile details page
+ Args:
+ request (HttpRequest): request object
+ username (str): username
+
+ Returns:
+ An HTTP response with the profile or HTTP response with 404 error if not found
+ or an HTTP response with 500 error
+ """
+ try:
+ profile = Profile.objects.get(user__username=username)
+ context = {'profile': profile}
+
+ logger.info(f"Going to profile details page : {context=}, status = 200.")
+
+ return render(request, template_name='profiles/profile.html', context=context, status=200)
+
+ except Profile.DoesNotExist as e:
+ context = {"type": "profile", "name": username, "error": str(e)}
+
+ logger.warning(f"Error 404 returned while reaching profile {username} : {context=},"
+ f" status = 404.")
+
+ return render(request, template_name='error_404.html', context=context, status=404)
+
+ except Exception as e:
+ context = {"error": str(e)}
+
+ logger.error(f"Error 500 returned while reaching profile details page : {context=},"
+ f" status = 500.")
+
+ return render(request, template_name='error_500.html', context=context, status=500)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000..fe5853fc61
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,25 @@
+[project]
+name = "openclassrooms-project-13"
+version = "0.1.0"
+description = ""
+authors = [
+ {name = "NM",email = "nicolas.marie.nm@gmail.com"}
+]
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "django (==3.0)",
+ "flake8 (==3.7.0)",
+ "flake8-html (==0.4.3)",
+ "pytest (==9.0.3)",
+ "pytest-django (==4.12.0)",
+ "pytest-cov (==7.1.0)",
+ "six (==1.17.0)",
+ "sentry-sdk (>=2.60.0,<3.0.0)",
+ "python-dotenv (>=1.2.2,<2.0.0)",
+]
+
+
+[build-system]
+requires = ["poetry-core>=2.0.0,<3.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/requirements.txt b/requirements.txt
index c48c84ea40..66496eaeec 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/setup.cfg b/setup.cfg
index 9346841bbc..8b4e36634a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,8 +1,11 @@
[flake8]
+format = html
+htmldir = flake8-report
max-line-length = 99
-exclude = **/migrations/*,venv
+exclude = **/migrations/*,env,cov_html
[tool:pytest]
DJANGO_SETTINGS_MODULE = oc_lettings_site.settings
python_files = tests.py
addopts = -v
+
diff --git a/templates/base.html b/templates/base.html
index ab7addba01..403b342755 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -24,10 +24,10 @@
diff --git a/templates/error_404.html b/templates/error_404.html
new file mode 100644
index 0000000000..bc8477aadb
--- /dev/null
+++ b/templates/error_404.html
@@ -0,0 +1,30 @@
+{% extends "base.html" %}
+{% block title %}Holiday Homes{% endblock title %}
+
+{% block content %}
+
+
+
+
+
+ {% if type == "letting" %}
+
+ {% elif type == "profile" %}
+
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/error_500.html b/templates/error_500.html
new file mode 100644
index 0000000000..f7d65b087f
--- /dev/null
+++ b/templates/error_500.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+{% block title %}Holiday Homes{% endblock title %}
+
+{% block content %}
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
index 71a8e61a46..fc9a76c7ab 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -14,10 +14,10 @@