Compare commits

..

19 Commits

Author SHA1 Message Date
d8068894bf build ok gunicorn+nginx 2025-09-15 11:12:08 +02:00
8095aa76ef try poetry-plugin-export 2025-09-15 11:07:49 +02:00
381f83af7e move key in .env 2025-09-15 10:51:00 +02:00
a43f42b4e4 add endline 2025-09-12 15:33:13 +02:00
3359cd63ce remove old print 2025-09-12 15:32:16 +02:00
f425673953 fix linting 2025-09-10 12:33:33 +02:00
f5c04f9d58 fix logging and exception 2025-09-10 12:32:36 +02:00
f6e8758541 unused, clean archi 2025-09-10 12:31:38 +02:00
7aeb92913c sentry logging on unknown querystring 2025-09-10 10:45:42 +02:00
286f1cb57b add docstrings 2025-09-09 16:32:32 +02:00
aca7042c56 add coverage and bs4 2025-09-09 16:20:39 +02:00
2abad89208 test models, urls, views - 100% 2025-09-09 16:20:15 +02:00
cb3af725b3 avoid testing useless files 2025-09-09 16:19:28 +02:00
dd5bccf708 add doctstings 2025-09-08 11:58:02 +02:00
2fd139de55 handle custom error templates 2025-09-08 09:59:16 +02:00
14fe0ded02 removed unused 2025-09-05 17:23:12 +02:00
c751602eca pep8 2025-09-05 17:11:36 +02:00
f609f2adbf Admin shouldn`t be changed (spec)
This reverts commit b40a7d9030.
2025-09-05 17:07:22 +02:00
b40a7d9030 display more 2025-09-05 16:56:43 +02:00
41 changed files with 797 additions and 326 deletions

6
.coveragerc Normal file
View File

@@ -0,0 +1,6 @@
[run]
source = .
omit = */test*,*000*,*conftest.py,*apps.py,*manage.py,*__init__.py,*asgi*,*wsgi*,*admin.py,*urls.py,*settings.py
[report]
omit = */test*,*000*,*conftest.py,*apps.py,*manage.py,*__init__.py,*asgi*,*wsgi*,*admin.py,*urls.py,*settings.py

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
FROM python:3.11-slim
RUN apt update && apt install -y nginx
RUN pip install poetry
WORKDIR /OCLettings2
COPY pyproject.toml poetry.lock ./
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
RUN poetry config virtualenvs.create false && \
poetry install --no-interaction --no-ansi --no-root && \
poetry add gunicorn
COPY . .
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /var/www/html* && \
mkdir -p /run/nginx
RUN poetry run python manage.py collectstatic --noinput
EXPOSE 80
CMD service nginx start && \
poetry run gunicorn --bind 127.0.0.1:8000 --env DJANGO_SETTINGS_MODULE=oc_lettings_site.settings oc_lettings_site.wsgi
#CMD ["poetry", "run", "gunicorn", "--bind", "0.0.0.0:8080", "--env", "DJANGO_SETTINGS_MODULE=oc_lettings_site.settings", "oc_lettings_site.wsgi"]

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -3,6 +3,17 @@ from django.core.validators import MaxValueValidator, MinLengthValidator
class Address(models.Model):
"""
Model of details for a physical location of a property
"""
class Meta:
"""
Fix the plural displayed in admin
"""
verbose_name_plural = "Addresses"
number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)])
street = models.CharField(max_length=64)
city = models.CharField(max_length=64)
@@ -11,13 +22,18 @@ class Address(models.Model):
country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)])
def __str__(self):
""" Display object with basic address """
return f'{self.number} {self.street}'
class Letting(models.Model):
"""
Model of an announce for a property to rent
"""
title = models.CharField(max_length=256)
address = models.OneToOneField(Address, on_delete=models.CASCADE)
def __str__(self):
""" display object with title """
return self.title

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,16 +1,35 @@
from django.shortcuts import render
from django.shortcuts import render, redirect
from lettings.models import Letting
import logging
logger = logging.getLogger(__name__)
def index(request):
"""
letting's index page. Retrieve all objects in db then give list to template
:param request; None
:return: render and display template HTML
"""
lettings_list = Letting.objects.all()
context = {'lettings_list': lettings_list}
return render(request, 'lettings/index.html', context)
def letting(request, letting_id):
letting = Letting.objects.get(id=letting_id)
context = {
'title': letting.title,
'address': letting.address,
}
return render(request, 'lettings/letting.html', context)
"""
display detail of a particular Letting object
:param request: None
:return: render and display template HTML
"""
try:
letting = Letting.objects.get(id=letting_id)
context = {
'title': letting.title,
'address': letting.address,
}
return render(request, 'lettings/letting.html', context)
except ValueError:
logger.error(f"letting id : {letting_id} not found")
return redirect('lettings_index')

22
nginx/nginx.conf Normal file
View File

@@ -0,0 +1,22 @@
upstream django {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name localhost;
location /static/ {
alias /OCLettings2/staticfiles/;
expires 30d;
}
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

Binary file not shown.

View File

@@ -2,6 +2,7 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oc_lettings_site.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE',
'oc_lettings_site.settings')
application = get_asgi_application()

View File

@@ -1 +0,0 @@

View File

@@ -1,6 +1,10 @@
import os
import sentry_sdk
from pathlib import Path
from dotenv import load_dotenv, dotenv_values
load_dotenv()
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -10,12 +14,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'fp$9^593hsriajg$_%=5trot9g!1qa@ew(o-1#@=&4%=hp46(s'
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = False
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ["*"]
# Application definition
@@ -79,16 +83,20 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
'NAME': 'django.contrib.auth.password_validation.'
'UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'NAME': 'django.contrib.auth.password_validation.'
'MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
'NAME': 'django.contrib.auth.password_validation.'
'CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
'NAME': 'django.contrib.auth.password_validation.'
'NumericPasswordValidator',
},
]
@@ -113,4 +121,11 @@ USE_TZ = True
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / "static",]
STATICFILES_DIRS = [BASE_DIR / "static"]
# Config Sentry
sentry_sdk.init(
dsn=os.getenv("SENTRY_URL"),
send_default_pii=True,
enable_logs=True,
)

View File

@@ -1,2 +0,0 @@
def test_dummy():
assert 1

View File

@@ -1,46 +1,10 @@
from django.shortcuts import render
from lettings.models import Letting
from profiles.models import 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):
"""
Main index of app, home page
:param request: None
:return: render and display homepage
"""
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)

434
poetry.lock generated
View File

@@ -12,9 +12,6 @@ files = [
{file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"},
]
[package.dependencies]
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
[package.extras]
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
@@ -49,6 +46,56 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-
tests = ["attrs[tests-no-zope]", "zope.interface"]
tests-no-zope = ["cloudpickle ; platform_python_implementation == \"CPython\"", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990) ; platform_python_implementation == \"CPython\"", "mypy (>=0.971,<0.990) ; platform_python_implementation == \"CPython\"", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version < \"3.11\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version < \"3.11\"", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
[[package]]
name = "beautifulsoup4"
version = "4.13.5"
description = "Screen-scraping library"
optional = false
python-versions = ">=3.7.0"
groups = ["main"]
files = [
{file = "beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a"},
{file = "beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695"},
]
[package.dependencies]
soupsieve = ">1.2"
typing-extensions = ">=4.0.0"
[package.extras]
cchardet = ["cchardet"]
chardet = ["chardet"]
charset-normalizer = ["charset-normalizer"]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]]
name = "bs4"
version = "0.0.2"
description = "Dummy package for Beautiful Soup (beautifulsoup4)"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"},
{file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"},
]
[package.dependencies]
beautifulsoup4 = "*"
[[package]]
name = "certifi"
version = "2025.8.3"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"},
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
]
[[package]]
name = "colorama"
version = "0.4.5"
@@ -62,6 +109,110 @@ files = [
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
[[package]]
name = "coverage"
version = "7.10.6"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356"},
{file = "coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301"},
{file = "coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460"},
{file = "coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd"},
{file = "coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb"},
{file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6"},
{file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945"},
{file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e"},
{file = "coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1"},
{file = "coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528"},
{file = "coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f"},
{file = "coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc"},
{file = "coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a"},
{file = "coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a"},
{file = "coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62"},
{file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153"},
{file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5"},
{file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619"},
{file = "coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba"},
{file = "coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e"},
{file = "coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c"},
{file = "coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea"},
{file = "coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634"},
{file = "coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6"},
{file = "coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9"},
{file = "coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c"},
{file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a"},
{file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5"},
{file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972"},
{file = "coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d"},
{file = "coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629"},
{file = "coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80"},
{file = "coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6"},
{file = "coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80"},
{file = "coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003"},
{file = "coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27"},
{file = "coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4"},
{file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d"},
{file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc"},
{file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc"},
{file = "coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e"},
{file = "coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32"},
{file = "coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2"},
{file = "coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b"},
{file = "coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393"},
{file = "coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27"},
{file = "coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df"},
{file = "coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb"},
{file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282"},
{file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4"},
{file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21"},
{file = "coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0"},
{file = "coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5"},
{file = "coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b"},
{file = "coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e"},
{file = "coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb"},
{file = "coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034"},
{file = "coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1"},
{file = "coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a"},
{file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb"},
{file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d"},
{file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747"},
{file = "coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5"},
{file = "coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713"},
{file = "coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32"},
{file = "coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65"},
{file = "coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6"},
{file = "coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0"},
{file = "coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e"},
{file = "coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5"},
{file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7"},
{file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5"},
{file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0"},
{file = "coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7"},
{file = "coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930"},
{file = "coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b"},
{file = "coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352"},
{file = "coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612"},
{file = "coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b"},
{file = "coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144"},
{file = "coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b"},
{file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862"},
{file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2"},
{file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78"},
{file = "coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c"},
{file = "coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf"},
{file = "coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3"},
{file = "coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90"},
]
[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"
@@ -83,6 +234,31 @@ sqlparse = ">=0.2.2"
argon2 = ["argon2-cffi (>=16.1.0)"]
bcrypt = ["bcrypt"]
[[package]]
name = "django-pytest"
version = "0.2.0"
description = "django test runner to use py.test tests"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "django-pytest-0.2.0.tar.gz", hash = "sha256:de21f20f9e7eb941529d75078b18192506a9f6d4ae80f86fbe2f3bcac8e09d71"},
]
[[package]]
name = "dotenv"
version = "0.9.9"
description = "Deprecated package"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"},
]
[package.dependencies]
python-dotenv = "*"
[[package]]
name = "entrypoints"
version = "0.3"
@@ -113,28 +289,6 @@ mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.5.0,<2.6.0"
pyflakes = ">=2.1.0,<2.2.0"
[[package]]
name = "importlib-metadata"
version = "4.8.3"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.6"
groups = ["main"]
markers = "python_version < \"3.8\""
files = [
{file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"},
{file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"},
]
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"]
perf = ["ipython"]
testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pep517", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)"]
[[package]]
name = "iniconfig"
version = "1.1.1"
@@ -161,37 +315,31 @@ files = [
[[package]]
name = "packaging"
version = "21.3"
version = "25.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "pluggy"
version = "1.0.0"
version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
]
[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "py"
@@ -229,21 +377,6 @@ files = [
{file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"},
]
[[package]]
name = "pyparsing"
version = "3.0.7"
description = "Python parsing module"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
]
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
version = "7.0.1"
@@ -260,7 +393,6 @@ files = [
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
@@ -270,6 +402,26 @@ tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "6.3.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749"},
{file = "pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2"},
]
[package.dependencies]
coverage = {version = ">=7.5", extras = ["toml"]}
pluggy = ">=1.2"
pytest = ">=6.2.5"
[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
[[package]]
name = "pytest-django"
version = "3.9.0"
@@ -289,6 +441,21 @@ pytest = ">=3.6"
docs = ["sphinx", "sphinx-rtd-theme"]
testing = ["Django", "django-configurations (>=2.0)", "six"]
[[package]]
name = "python-dotenv"
version = "1.1.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"},
{file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "pytz"
version = "2025.2"
@@ -301,6 +468,89 @@ files = [
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
]
[[package]]
name = "sentry-sdk"
version = "2.37.1"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "sentry_sdk-2.37.1-py2.py3-none-any.whl", hash = "sha256:baaaea6608ed3a639766a69ded06b254b106d32ad9d180bdbe58f3db9364592b"},
{file = "sentry_sdk-2.37.1.tar.gz", hash = "sha256:531751da91aa62a909b42a7be155b41f6bb0de9df6ae98441d23b95de2f98475"},
]
[package.dependencies]
certifi = "*"
django = {version = ">=1.8", optional = true, markers = "extra == \"django\""}
urllib3 = ">=1.26.11"
[package.extras]
aiohttp = ["aiohttp (>=3.5)"]
anthropic = ["anthropic (>=0.16)"]
arq = ["arq (>=0.23)"]
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"]
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)"]
litestar = ["litestar (>=2.0.0)"]
loguru = ["loguru (>=0.5)"]
openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"]
openfeature = ["openfeature-sdk (>=0.7.1)"]
opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
opentelemetry-experimental = ["opentelemetry-distro"]
pure-eval = ["asttokens", "executing", "pure_eval"]
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 = "soupsieve"
version = "2.8"
description = "A modern CSS selector implementation for Beautiful Soup."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"},
{file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"},
]
[[package]]
name = "sqlparse"
version = "0.4.4"
@@ -320,47 +570,77 @@ test = ["pytest", "pytest-cov"]
[[package]]
name = "tomli"
version = "1.2.3"
version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
{file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
]
[[package]]
name = "typing-extensions"
version = "4.1.1"
description = "Backported and Experimental Type Hints for Python 3.6+"
version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version < \"3.8\""
files = [
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
{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 = "zipp"
version = "3.6.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
name = "urllib3"
version = "2.5.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.6"
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version < \"3.8\""
files = [
{file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
{file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
{file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
{file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
]
[package.extras]
docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"]
testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy ; platform_python_implementation != \"PyPy\""]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.6"
content-hash = "dc98175b1ff18844811121b4489b45c63c3890bbc11145325f802073521b7b71"
python-versions = ">=3.9, <4.0"
content-hash = "ff2e5437fe8f89479484f9df2dea16b0893c8dcee4f7641cf7af85becf2d1379"

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -3,9 +3,11 @@ from django.contrib.auth.models import User
class Profile(models.Model):
"""
Model of a user of the service
"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
favorite_city = models.CharField(max_length=64, blank=True)
def __str__(self):
return self.user.username

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,13 +1,32 @@
from django.shortcuts import render
from django.shortcuts import render, redirect
from profiles.models import Profile
import logging
logger = logging.getLogger(__name__)
def index(request):
"""
Display the list of all profiles
:param request: None
:return: render and display template as HTML
"""
profiles_list = Profile.objects.all()
context = {'profiles_list': profiles_list}
return render(request, 'profiles/index.html', context)
def profile(request, username):
profile = Profile.objects.get(user__username=username)
context = {'profile': profile}
return render(request, 'profiles/profile.html', context)
"""
Display the detail of a give profile
:param request: None
:return: render and display template as HTML
"""
try:
profile = Profile.objects.get(user__username=username)
context = {'profile': profile}
return render(request, 'profiles/profile.html', context)
except (ValueError, Profile.DoesNotExist):
logger.error(f"Username : {username} doesn't exist")
return redirect('profiles_index')

View File

@@ -6,11 +6,17 @@ authors = [
{name = "Your Name",email = "you@example.com"}
]
readme = "README.md"
requires-python = ">=3.6"
requires-python = ">=3.9, <4.0"
dependencies = [
"django (==3.0)",
"flake8 (==3.7.0)",
"pytest-django (==3.9.0)"
"pytest-django (==3.9.0)",
"django-pytest (>=0.2.0,<0.3.0)",
"six (>=1.17.0,<2.0.0)",
"pytest-cov (>=6.3.0,<7.0.0)",
"bs4 (>=0.0.2,<0.0.3)",
"sentry-sdk[django] (>=2.37.1,<3.0.0)",
"dotenv (>=0.9.9,<0.10.0)",
]

2
pytest.ini Normal file
View File

@@ -0,0 +1,2 @@
[pytest]
DJANGO_SETTINGS_MODULE = oc_lettings_site.settings

25
templates/404.html Normal file
View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}404 Not Found{% endblock title %}
{% block content %}
<div class="container px-5 py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="page-header-ui-title mb-3 display-6">Error - 404</h1>
<h2 class="page-header-ui-title mb-3 display-6">Page Not Found</h2>
</div>
</div>
</div>
<div class="container px-5 py-5 text-center">
<div class="justify-content-center">
<p>An error has occurred</p>
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'index' %}">
Back Home
</a>
</div>
</div>
{% endblock %}

24
templates/500.html Normal file
View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}404 Not Found{% endblock title %}
{% block content %}
<div class="container px-5 py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="page-header-ui-title mb-3 display-6">Error - 500</h1>
<h2 class="page-header-ui-title mb-3 display-6">Server Issue</h2>
</div>
</div>
</div>
<div class="container px-5 py-5 text-center">
<div class="justify-content-center">
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'index' %}">
Back Home
</a>
</div>
</div>
{% endblock %}

View File

@@ -1,41 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock title %}
{% block content %}
<div class="container px-5 py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="page-header-ui-title mb-3 display-6">{{ title }}</h1>
</div>
</div>
</div>
<div class="container px-5 py-5 text-center">
<div class="card">
<div class="card-body">
<div class="icon-stack icon-stack-lg bg-primary text-white mb-3"><i data-feather="home"></i></div>
<p>{{ address.number }} {{ address.street }}</p>
<p>{{ address.city }}, {{ address.state }} {{ address.zip_code }}</p>
<p>{{ address.country_iso_code }}</p>
</div>
</div>
</div>
<div class="container px-5 py-5 text-center">
<div class="justify-content-center">
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'lettings_index' %}">
<i class="ms-2" data-feather="arrow-right"></i>
Back
</a>
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'index' %}">
Home
</a>
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'profiles_index' %}">
Profiles
</a>
</div>
</div>
{% endblock %}

View File

@@ -1,45 +0,0 @@
{% extends "base.html" %}
{% block title %}Lettings{% endblock title %}
{% block content %}
<div class="container px-5 py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="page-header-ui-title mb-3 display-6">Lettings</h1>
</div>
</div>
</div>
<div class="container px-5">
<div class="row gx-5 justify-content-center">
<div class="col-lg-10">
<hr class="mb-0" />
{% if lettings_list %}
<ul class="list-group list-group-flush list-group-careers">
{% for letting in lettings_list %}
<li class="list-group-item">
<a href="{% url 'letting' letting_id=letting.id %}">{{ letting.title }}</a>
</li>
{% endfor %}
</ul>
{% else %}
<p>No lettings are available.</p>
{% endif %}
</div>
</div>
</div>
<div class="container px-5 py-5 text-center">
<div class="justify-content-center">
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'index' %}">
Home
</a>
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'profiles_index' %}">
Profiles
</a>
</div>
</div>
{% endblock %}

View File

@@ -1,40 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ profile.user.username }}{% endblock title %}
{% block content %}
<div class="container px-5 py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="page-header-ui-title mb-3 display-6">{{ profile.user.username }}</h1>
</div>
</div>
</div>
<div class="container px-5 py-5 text-center">
<div class="card">
<div class="card-body">
<div class="icon-stack icon-stack-lg bg-primary text-white mb-3"><i data-feather="user"></i></div>
<p><strong>First name :</strong> {{ profile.user.first_name }}</p>
<p><strong>Last name :</strong> {{ profile.user.last_name }}</p>
<p><strong>Email :</strong> {{ profile.user.email }}</p>
<p><strong>Favorite city :</strong> {{ profile.favorite_city }}</p>
</div>
</div>
</div>
<div class="container px-5 py-5 text-center">
<div class="justify-content-center">
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'profiles_index' %}">
<i class="ms-2" data-feather="arrow-left"></i>
Back
</a>
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'index' %}">
Home
</a>
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'lettings_index' %}">
Lettings
</a>
</div>
</div>
{% endblock %}

View File

@@ -1,43 +0,0 @@
{% extends "base.html" %}
{% block title %}Profiles{% endblock title %}
{% block content %}
<div class="container px-5 py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="page-header-ui-title mb-3 display-6">Profiles</h1>
</div>
</div>
</div>
<div class="container px-5">
<div class="row gx-5 justify-content-center">
<div class="col-lg-10">
<hr class="mb-0" />
{% if profiles_list %}
<ul class="list-group list-group-flush list-group-careers">
{% for profile in profiles_list %}
<li class="list-group-item">
<a href="{% url 'profile' username=profile.user.username %}">{{ profile.user.username }}</a>
</li>
{% endfor %}
</ul>
{% else %}
<p>No profiles are available.</p>
{% endif %}
</div>
</div>
</div>
<div class="container px-5 py-5 text-center">
<div class="justify-content-center">
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'index' %}">
Home
</a>
<a class="btn fw-500 ms-lg-4 btn-primary px-10" href="{% url 'lettings_index' %}">
Lettings
</a>
</div>
</div>
{% endblock %}

BIN
tests/.coverage Normal file

Binary file not shown.

0
tests/__init__.py Normal file
View File

83
tests/conftest.py Normal file
View File

@@ -0,0 +1,83 @@
import pytest
from lettings.models import Address, Letting
from profiles.models import Profile
from django.contrib.auth.models import User
@pytest.fixture
@pytest.mark.django_db
def sample_address():
"""
creates temporary Addresses objects in test DB
:return: tuple of Address
"""
address1 = Address.objects.create(
number=22,
street="Quality Street",
city="New-York",
state="New-York",
zip_code=10010,
country_iso_code="US"
)
address2 = Address.objects.create(
number=18,
street="Rue Cocotte",
city="Paris",
state="Ile-de-France",
zip_code=75000,
country_iso_code="FR"
)
return address1, address2
@pytest.fixture
@pytest.mark.django_db
def sample_letting(sample_address):
"""
creates temporary Letting objects in test DB
:return: tuple of Lettings
"""
letting = Letting.objects.create(
title="Pretty thing",
address=sample_address[0],
)
letting2 = Letting.objects.create(
title="Ugly thing",
address=sample_address[1],
)
return letting, letting2
@pytest.fixture
@pytest.mark.django_db
def sample_profile():
"""
creates temporary Profile objects in test DB
:return: tuple of Profiles
"""
user = User.objects.create(
username='TestUser',
password='password',
)
user2 = User.objects.create(
username='TestUser2',
password='password2',
)
user3 = User.objects.create(
username='TestUser3',
password='password2',
)
profile = Profile.objects.create(
user=user,
favorite_city="Paris",
)
profile2 = Profile.objects.create(
user=user2,
favorite_city="Marseilles",
)
profile3 = Profile.objects.create(
user=user3,
favorite_city="Rennes",
)
return profile, profile2, profile3

0
tests/unit/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,15 @@
import pytest
@pytest.mark.django_db
def test_address_str(sample_address):
""" test if Address objects are well created """
assert str(sample_address[0]) == "22 Quality Street"
assert str(sample_address[1]) == "18 Rue Cocotte"
@pytest.mark.django_db
def test_letting_str2(sample_letting):
""" test if Letting objects are well created """
assert str(sample_letting[0]) == "Pretty thing"
assert str(sample_letting[1]) == "Ugly thing"

View File

@@ -0,0 +1,21 @@
import pytest
from django.urls import reverse
from django.test import Client
@pytest.mark.django_db
def test_should_get_200_on_lettings_index():
""" test the server's response """
c = Client()
url = reverse('lettings_index')
response = c.get(url)
assert response.status_code == 200
@pytest.mark.django_db
def test_should_get_200_on_letting_detail(sample_letting):
c = Client()
url = reverse('letting', kwargs={'letting_id':1})
response = c.get(url)
assert response.status_code == 200

View File

@@ -0,0 +1,19 @@
import pytest
from lettings.models import Address, Letting
from django.test import Client
from django.urls import reverse
from bs4 import BeautifulSoup
@pytest.mark.django_db
def test_view_should_display_right_len_list(sample_letting):
"""
test if the list displayed contains the right amount of objects
created in fixture
"""
c = Client()
url = reverse('lettings_index')
response = c.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
li_tags = soup.find_all('li')
assert len(li_tags) == 2

View File

View File

@@ -0,0 +1,18 @@
from django.urls import reverse, resolve
from oc_lettings_site.views import index
from django.test import Client
def test_server_should_answer_200():
""" test the server's response on home """
c = Client()
url = reverse('index')
response = c.get(url)
assert response.status_code == 200
def test_home_url():
""" test the home url """
url = reverse('index')
assert resolve(url).view_name == 'index'
assert resolve(url).func, index()

View File

@@ -0,0 +1,10 @@
from django.test import Client
from django.urls import reverse
def test_view_should_reply_title_on_home():
""" test the content display (title) """
c = Client()
url = reverse('index')
response = c.get(url)
assert "Welcome to Holiday Homes</h1>" in response.content.decode()

View File

View File

@@ -0,0 +1,16 @@
import pytest
from django.contrib.auth.models import User
from profiles.models import Profile
@pytest.mark.django_db
def test_str_profile():
user = User.objects.create(
username='TestUser',
password='password',
)
profile = Profile.objects.create(
user=user,
favorite_city="Paris",
)
assert str(profile) == "TestUser"

View File

@@ -0,0 +1,22 @@
import pytest
from django.urls import reverse
from django.test import Client
@pytest.mark.django_db
def test_should_get_200_on_profile_index():
""" test the server's response on profile index page """
c = Client()
url = reverse('profiles_index')
response = c.get(url)
assert response.status_code == 200
@pytest.mark.django_db
def test_should_get_200_on_profile_detail(sample_profile):
""" test the server's response on a profile detail page """
c = Client()
url = reverse('profile', kwargs={'username': "TestUser"})
response = c.get(url)
assert response.status_code == 200

View File

@@ -0,0 +1,18 @@
import pytest
from django.test import Client
from django.urls import reverse
from bs4 import BeautifulSoup
@pytest.mark.django_db
def test_view_should_display_right_len_list(sample_profile):
"""
test that html page displays the right amount of objects
created by fixture
"""
c = Client()
url = reverse('profiles_index')
response = c.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
li_tags = soup.find_all('li')
assert len(li_tags) == 3