diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000..8692df254a Binary files /dev/null and b/.coverage differ diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..624291b553 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +[run] +omit = + */tests.py + */tests/* + */test_*.py + */tests_*.py + */migrations/* + */apps.py + */__init__.py + */admin.py + */asgi.py + */wsgi.py + */settings.py \ No newline at end of file diff --git a/.gitignore b/.gitignore index b4405ebab4..afbe75074a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,21 @@ +# Temp files **/__pycache__ *.pyc + +# Virtual environment venv +env + +# Idea +.idea + +# Database +backup.sqlite3 +oc-lettings-site_old.sqlite3 + +# Reports +flake8-report +cov_html + +# .env file +.env \ No newline at end of file diff --git a/README.md b/README.md index c8547803f7..6546bc4914 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,188 @@ -## Résumé +# Django App - OpenClassrooms Project 13 +**Scale a Django application using a modular architecture** -Site web d'Orange County Lettings +--- -## Développement local +## DESCRIPTION -### Prérequis +This project was completed as part of the "Python Developer" path at OpenClassrooms. -- Compte GitHub avec accès en lecture à ce repository -- Git CLI -- SQLite3 CLI -- Interpréteur Python, version 3.6 ou supérieure +The goal was to scale a Django application using a modular architecture : -Dans le reste de la documentation sur le développement local, il est supposé que la commande `python` de votre OS shell exécute l'interpréteur Python ci-dessus (à moins qu'un environnement virtuel ne soit activé). +- Redesign of the modular architecture in the GitHub repository; +- Reduction of various technical debts on the project; +- Addition and deployment of a CI/CD pipeline; +- Application monitoring and error tracking via Sentry; +- Creation of the application's technical documentation using Read The Docs and Sphinx. -### macOS / Linux +The application must: -#### Cloner le repository +- allow the users to view available rentals and all the registered profiles. -- `cd /path/to/put/project/in` -- `git clone https://github.com/OpenClassrooms-Student-Center/Python-OC-Lettings-FR.git` +--- -#### Créer l'environnement virtuel +## PROJECT STRUCTURE +

+ +

-- `cd /path/to/Python-OC-Lettings-FR` -- `python -m venv venv` -- `apt-get install python3-venv` (Si l'étape précédente comporte des erreurs avec un paquet non trouvé sur Ubuntu) -- Activer l'environnement `source venv/bin/activate` -- Confirmer que la commande `python` exécute l'interpréteur Python dans l'environnement virtuel -`which python` -- Confirmer que la version de l'interpréteur Python est la version 3.6 ou supérieure `python --version` -- Confirmer que la commande `pip` exécute l'exécutable pip dans l'environnement virtuel, `which pip` -- Pour désactiver l'environnement, `deactivate` +--- -#### Exécuter le site +## INSTALLATION -- `cd /path/to/Python-OC-Lettings-FR` -- `source venv/bin/activate` -- `pip install --requirement requirements.txt` -- `python manage.py runserver` -- Aller sur `http://localhost:8000` dans un navigateur. -- Confirmer que le site fonctionne et qu'il est possible de naviguer (vous devriez voir plusieurs profils et locations). +- ### Clone the repository : -#### Linting +``` +git clone https://github.com/Tit-Co/OpenClassrooms_Project_13.git +``` -- `cd /path/to/Python-OC-Lettings-FR` -- `source venv/bin/activate` -- `flake8` +- ### Navigate into the project directory : + `cd OpenClassrooms_Project_13` -#### Tests unitaires +- ### Create a virtual environment and dependencies : -- `cd /path/to/Python-OC-Lettings-FR` -- `source venv/bin/activate` -- `pytest` +1. #### With [uv](https://docs.astral.sh/uv/) -#### Base de données + `uv` is an environment and dependencies manager. + + - #### Install environment and dependencies + + `uv sync` -- `cd /path/to/Python-OC-Lettings-FR` -- Ouvrir une session shell `sqlite3` -- Se connecter à la base de données `.open oc-lettings-site.sqlite3` -- Afficher les tables dans la base de données `.tables` -- Afficher les colonnes dans le tableau des profils, `pragma table_info(Python-OC-Lettings-FR_profile);` -- Lancer une requête sur la table des profils, `select user_id, favorite_city from - Python-OC-Lettings-FR_profile where favorite_city like 'B%';` -- `.quit` pour quitter +2. #### With pip -#### Panel d'administration + - #### Install the virtual env : -- Aller sur `http://localhost:8000/admin` -- Connectez-vous avec l'utilisateur `admin`, mot de passe `Abc1234!` + `python -m venv env` -### Windows + - #### Activate the virtual env : + `source env/bin/activate` in Git Bash on Windows or on macOS / Linux + Or + `env\Scripts\activate` on Windows -Utilisation de PowerShell, comme ci-dessus sauf : +3. #### With [Poetry](https://python-poetry.org/docs/) -- Pour activer l'environnement virtuel, `.\venv\Scripts\Activate.ps1` -- Remplacer `which ` par `(Get-Command ).Path` + `Poetry` is a tool for dependency management and packaging in Python. + + - #### Install the virtual env : + `py -3.10 -m venv env` + + - #### Activate the virtual env : + `poetry env activate` + +- ### Install dependencies + 1. #### With [uv](https://docs.astral.sh/uv/) + `uv sync` or `uv pip install -r requirements.txt` or `uv add -r requirements.txt` + + 2. #### With pip + `pip install -r requirements.txt` + + 3. #### With [Poetry](https://python-poetry.org/docs/) + `poetry install` + + (NB : Poetry and uv will read the `pyproject.toml` file to know which dependencies to install) + +--- + +## USAGE + +### Launching server +- Open a terminal +- Go to project folder - example : `cd oc_lettings_site` +- Activate the virtual environment as described previously +- Create environment variables (to avoid to add raw Sentry key into the code) + - With Power Shell : + ``` + $env:SENTRY_KEY = "my_key" + ``` + - With Git Bash : + ``` + export SENTRY_KEY = "my_key" + ``` +- Launch the local server by typing the command : + - `python manage.py runserver` + +### Launching the APP + +- Finally, in a web browser, open the urls : + - [http://127.0.0.1:8000/](http://127.0.0.1:8000/) + - [http://127.0.0.1:8000/admin](http://127.0.0.1:8000/admin) + - for the admin panel (username: ```admin```, password: ```Abc1234!```) + +--- + +## APP EXAMPLES + +Here are some examples of the application execution. + +- Home page +

+ +

+ +- Lettings index +

+ +

+ +- Letting details +

+ +

+ +- Profiles index +

+ +

+ +- Profile details +

+ +

+ +--- + +## PEP 8 CONVENTIONS + +- Flake 8 report +

+ +

+ +**Type the line below in the terminal to generate another report with [flake8-html](https://pypi.org/project/flake8-html/) tool :** + +` flake8` +- The app code has a setup.cfg file that specify Flake 8 options as below : + ``` + format = html + htmldir = flake8-report + max-line-length = 99 + exclude = **/migrations/*,env,cov_html + ``` + +--- + +## TESTS COVERAGE WITH PYTEST + +- Coverage report +

+ + +

+ +- **Type the line below in the terminal to generate another coverage report with pytest** + + `pytest --cov=lettings --cov=profiles --cov=oc_lettings_site --cov-report=html:cov_html` + +--- + +![Python](https://img.shields.io/badge/python-3.10-blue.svg) +![License](https://img.shields.io/badge/license-MIT-green.svg) +![Coverage](https://img.shields.io/badge/coverage-100%25-red) + +--- + +## AUTHOR +**Name**: Nicolas MARIE +**Track**: Python Developer – OpenClassrooms +**Project 13 – Scale a Django application using a modular architecture – May 2026** diff --git a/config.py b/config.py new file mode 100644 index 0000000000..097854e7fc --- /dev/null +++ b/config.py @@ -0,0 +1,11 @@ +""" +Config module for environment variables +""" +import os + +from dotenv import load_dotenv + + +load_dotenv() + +SENTRY_KEY = os.getenv("SENTRY_KEY") diff --git a/docs/cov_report_1.png b/docs/cov_report_1.png new file mode 100644 index 0000000000..8da9ced1d2 Binary files /dev/null and b/docs/cov_report_1.png differ diff --git a/docs/cov_report_2.png b/docs/cov_report_2.png new file mode 100644 index 0000000000..58c9a2c22e Binary files /dev/null and b/docs/cov_report_2.png differ diff --git a/docs/flake8_report.png b/docs/flake8_report.png new file mode 100644 index 0000000000..3736cee381 Binary files /dev/null and b/docs/flake8_report.png differ diff --git a/docs/screenshots/home_page_screenshot.png b/docs/screenshots/home_page_screenshot.png new file mode 100644 index 0000000000..798f0950ee Binary files /dev/null and b/docs/screenshots/home_page_screenshot.png differ diff --git a/docs/screenshots/letting_details_screenshot.png b/docs/screenshots/letting_details_screenshot.png new file mode 100644 index 0000000000..ab0dbd83bb Binary files /dev/null and b/docs/screenshots/letting_details_screenshot.png differ diff --git a/docs/screenshots/lettings_list_screenshot.png b/docs/screenshots/lettings_list_screenshot.png new file mode 100644 index 0000000000..21f41d766f Binary files /dev/null and b/docs/screenshots/lettings_list_screenshot.png differ diff --git a/docs/screenshots/profile_details_screenshot.png b/docs/screenshots/profile_details_screenshot.png new file mode 100644 index 0000000000..57ca31328d Binary files /dev/null and b/docs/screenshots/profile_details_screenshot.png differ diff --git a/docs/screenshots/profiles_list_screenshot.png b/docs/screenshots/profiles_list_screenshot.png new file mode 100644 index 0000000000..835b72dbdd Binary files /dev/null and b/docs/screenshots/profiles_list_screenshot.png differ diff --git a/lettings/__init__.py b/lettings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/admin.py b/lettings/admin.py new file mode 100644 index 0000000000..2b5abe26f8 --- /dev/null +++ b/lettings/admin.py @@ -0,0 +1,10 @@ +""" +Admin registration module for lettings app +""" +from django.contrib import admin + +from lettings.models import Address, Letting + + +admin.site.register(Letting) +admin.site.register(Address) diff --git a/lettings/apps.py b/lettings/apps.py new file mode 100644 index 0000000000..9a30d0d289 --- /dev/null +++ b/lettings/apps.py @@ -0,0 +1,13 @@ +""" +Namespace module for lettings app +""" +from django.apps import AppConfig + + +class LettingsConfig(AppConfig): + """ + Namespace class for Lettings + Attributes: + name: namespace of the lettings app + """ + name = 'lettings' diff --git a/lettings/migrations/0001_initial.py b/lettings/migrations/0001_initial.py new file mode 100644 index 0000000000..9a008d3d77 --- /dev/null +++ b/lettings/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 3.0 on 2026-05-18 12:53 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(9999)])), + ('street', models.CharField(max_length=64)), + ('city', models.CharField(max_length=64)), + ('state', models.CharField(max_length=2, validators=[django.core.validators.MinLengthValidator(2)])), + ('zip_code', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(99999)])), + ('country_iso_code', models.CharField(max_length=3, validators=[django.core.validators.MinLengthValidator(3)])), + ], + ), + migrations.CreateModel( + name='Letting', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=256)), + ('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='new_letting', to='lettings.Address')), + ], + ), + ] diff --git a/lettings/migrations/0002_auto_20260518_1543.py b/lettings/migrations/0002_auto_20260518_1543.py new file mode 100644 index 0000000000..47cb6b8db2 --- /dev/null +++ b/lettings/migrations/0002_auto_20260518_1543.py @@ -0,0 +1,48 @@ +# Generated by Django 3.0 on 2026-05-18 13:43 + +from django.db import migrations + + +def copy_address(apps, schema_editor): + OldAddress = apps.get_model('oc_lettings_site', 'Address') + NewAddress = apps.get_model('lettings', 'Address') + + for obj in OldAddress.objects.all(): + NewAddress.objects.create( + id=obj.id, + number=obj.number, + street=obj.street, + city=obj.city, + state=obj.state, + zip_code=obj.zip_code, + country_iso_code=obj.country_iso_code, + ) + + +def copy_lettings(apps, schema_editor): + OldLetting = apps.get_model('oc_lettings_site', 'Letting') + NewLetting = apps.get_model('lettings', 'Letting') + NewAddress = apps.get_model('lettings', 'Address') + + for obj in OldLetting.objects.all(): + + new_address = NewAddress.objects.get(id=obj.address.id) + + NewLetting.objects.create( + id=obj.id, + title=obj.title, + address=new_address, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0001_initial'), + ('oc_lettings_site', '0001_initial'), + ] + + operations = [ + migrations.RunPython(copy_address), + migrations.RunPython(copy_lettings), + ] diff --git a/lettings/migrations/0003_auto_20260518_1551.py b/lettings/migrations/0003_auto_20260518_1551.py new file mode 100644 index 0000000000..11b098bd67 --- /dev/null +++ b/lettings/migrations/0003_auto_20260518_1551.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0 on 2026-05-18 13:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0002_auto_20260518_1543'), + ] + + operations = [ + migrations.AlterField( + model_name='letting', + name='address', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='lettings.Address'), + ), + ] diff --git a/lettings/migrations/__init__.py b/lettings/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/models.py b/lettings/models.py new file mode 100644 index 0000000000..081de2a663 --- /dev/null +++ b/lettings/models.py @@ -0,0 +1,58 @@ +""" +Models module for lettings app +""" +from django.db import models +from django.core.validators import MaxValueValidator, MinLengthValidator + + +class Address(models.Model): + """ + Address model for Lettings + Attributes: + number (int): Letting number + street (str): Street address + city (str): City address + state (str): State address + zip_code (int): Zip code + country_iso_code (int): Country code + """ + 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)]) + + class Meta: + """ + Meta class for Lettings to specify verbose names + """ + verbose_name = "Address" + verbose_name_plural = "Addresses" + + def __str__(self) -> str: + """ + string method for Lettings + Returns: + A f-string with number and street address + """ + return f'{self.number} {self.street}' + + +class Letting(models.Model): + """ + Letting model for Lettings + Attributes: + title (str): Letting title + address (Address): Letting address + """ + title = models.CharField(max_length=256) + address = models.OneToOneField(Address, on_delete=models.CASCADE) + + def __str__(self) -> str: + """ + String method for Lettings + Returns: + A f-string with letting title + """ + return self.title diff --git a/templates/lettings_index.html b/lettings/templates/lettings/index.html similarity index 89% rename from templates/lettings_index.html rename to lettings/templates/lettings/index.html index 92857a78d9..a85f3a348e 100644 --- a/templates/lettings_index.html +++ b/lettings/templates/lettings/index.html @@ -20,7 +20,7 @@

Lettings

@@ -36,7 +36,7 @@

Lettings

Home - + Profiles diff --git a/templates/letting.html b/lettings/templates/lettings/letting.html similarity index 95% rename from templates/letting.html rename to lettings/templates/lettings/letting.html index 7e5f3a73fd..252d68035e 100644 --- a/templates/letting.html +++ b/lettings/templates/lettings/letting.html @@ -25,14 +25,14 @@

{{ title }}

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 = '

Lettings

' + 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'

500 Error : ' + f'something wrong with the server - forced error

') + + 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'

{get_letting.title}

' + 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'

404 Error : ' + f'letting n\xb0 2 not found !

') + + 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'

500 Error : ' + f'something wrong with the server - forced error

') + + 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'

Welcome to Holiday ' + f'Homes

') + + 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'

500 Error : ' + f'something wrong with the server - forced error

') + + 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 @@

Profiles

@@ -34,7 +34,7 @@

Profiles

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 @@

{{ profile.user.username }}

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 = '

Profiles

' + 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'

500 Error : ' + f'something wrong with the server - forced error

') + + 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'

' + f'{get_profile.user.username}

') + 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'

404 Error : ' + f'profile \'test\' not found !

') + + 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'

500 Error : ' + f'something wrong with the server - forced error

') + + 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 @@
Logo Orange County Lettings 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" %} +

404 Error : {{ type }} n° {{ id }} not found !

+ {% elif type == "profile" %} +

404 Error : {{ type }} '{{ name }}' not found !

+ {% 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 %} + + +
+
+
+

500 Error : something wrong with the server - {{ error }}

+
+
+
+ + + +{% 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 @@

Welcome to Holiday Homes