This commit is contained in:
ilia 2023-05-21 13:37:21 +03:00
parent 63d4a0a850
commit 4f5e597258
32 changed files with 388 additions and 13 deletions

2
.gitignore vendored
View File

@ -330,4 +330,4 @@ passfinder/media/
.ipython/ .ipython/
.env .env
.idea .idea

View File

@ -1,2 +1,10 @@
from rest_framework.routers import DefaultRouter
from passfinder.recomendations.api.views import TinderView
router = DefaultRouter()
router.register('tinder', TinderView)
app_name = "api" app_name = "api"
urlpatterns = [] urlpatterns = router.urls

View File

@ -39,7 +39,7 @@
# DATABASES # DATABASES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#databases # https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {"default": env.db("DATABASE_URL")} DATABASES = {"default": env.db("DATABASE_URL", "postgres://postgres:Ilvas2006@localhost:5432/passfinder")}
DATABASES["default"]["ATOMIC_REQUESTS"] = True DATABASES["default"]["ATOMIC_REQUESTS"] = True
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@ -77,6 +77,7 @@
LOCAL_APPS = [ LOCAL_APPS = [
"passfinder.users", "passfinder.users",
"passfinder.events", "passfinder.events",
"passfinder.recomendations"
] ]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
@ -284,9 +285,9 @@
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-timezone # https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-timezone
CELERY_TIMEZONE = TIME_ZONE CELERY_TIMEZONE = TIME_ZONE
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-broker_url # https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-broker_url
CELERY_BROKER_URL = env("CELERY_BROKER_URL") #CELERY_BROKER_URL = env("CELERY_BROKER_URL")
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-result_backend # https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-result_backend
CELERY_RESULT_BACKEND = CELERY_BROKER_URL #CELERY_RESULT_BACKEND = CELERY_BROKER_URL
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-extended # https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-extended
CELERY_RESULT_EXTENDED = True CELERY_RESULT_EXTENDED = True
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-backend-always-retry # https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-backend-always-retry

View File

@ -43,11 +43,11 @@
} }
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"]
if env("USE_DOCKER") == "yes": # if env("USE_DOCKER") == "yes":
import socket # import socket
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) # hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips] # INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
# django-extensions # django-extensions
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from passfinder.events.models import Hotel, HotelPhone, City from passfinder.events.models import Hotel, HotelPhone, City, Event
class HotelPhoneSerializer(serializers.ModelSerializer): class HotelPhoneSerializer(serializers.ModelSerializer):
@ -29,3 +29,9 @@ class MuseumSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Hotel model = Hotel
exclude = "oid" exclude = "oid"
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ('type', 'title', 'description', 'city', 'oid')

View File

View File

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

View File

@ -0,0 +1,7 @@
from rest_framework import serializers
from passfinder.events.api.serializers import EventSerializer
class TinderProceedSerializer(serializers.Serializer):
action = serializers.ChoiceField(['left', 'right'], write_only=True)
event = EventSerializer(read_only=True)

View File

@ -0,0 +1,26 @@
from typing import Any
from rest_framework import viewsets, mixins
from rest_framework.request import Request
from rest_framework.response import Response
from passfinder.events.models import Event
from passfinder.events.api.serializers import EventSerializer
from random import choice
from rest_framework.decorators import action
from rest_framework.response import Response
from .serializers import TinderProceedSerializer
class TinderView(viewsets.GenericViewSet):
serializer_class = EventSerializer
model = Event
queryset = Event.objects.all()
@action(methods=['GET'], detail=False, serializer_class=EventSerializer)
def start(self, request: Request, *args: Any, **kwargs: Any):
event = EventSerializer(choice(Event.objects.all()))
return Response(data=event.data, status=200)
@action(methods=['POST'], detail=True, serializer_class=TinderProceedSerializer)
def proceed(self, request: Request, *args: Any, **kwargs: Any):
event = EventSerializer(choice(Event.objects.all()))
return Response(data={'event': event.data}, status=200)

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class RecomendationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "passfinder.recomendations"

View File

@ -0,0 +1,75 @@
# Generated by Django 4.2.1 on 2023-05-21 10:36
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("events", "0011_remove_event_purchase_method_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="UserPreferences",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"preferred_concerts",
models.ManyToManyField(
related_name="preffered_users_concert", to="events.event"
),
),
(
"preffered_movies",
models.ManyToManyField(
related_name="preffered_user_movie", to="events.event"
),
),
(
"preffered_plays",
models.ManyToManyField(
related_name="preffered_user_play", to="events.event"
),
),
(
"unpreferred_concerts",
models.ManyToManyField(
related_name="unpreffered_users_concert", to="events.event"
),
),
(
"unpreffered_lays",
models.ManyToManyField(
related_name="unpreffered_users_play", to="events.event"
),
),
(
"unpreffered_movies",
models.ManyToManyField(
related_name="unpreffered_user_movie", to="events.event"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@ -0,0 +1,16 @@
from django.db import models
from passfinder.users.models import User
from passfinder.events.models import Event
class UserPreferences(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
preffered_plays = models.ManyToManyField(Event, related_name='preffered_user_play')
unpreffered_lays = models.ManyToManyField(Event, related_name='unpreffered_users_play')
preffered_movies = models.ManyToManyField(Event, related_name='preffered_user_movie')
unpreffered_movies = models.ManyToManyField(Event, related_name='unpreffered_user_movie')
preferred_concerts = models.ManyToManyField(Event, related_name='preffered_users_concert')
unpreferred_concerts = models.ManyToManyField(Event, related_name='unpreffered_users_concert')

Binary file not shown.

View File

@ -0,0 +1,26 @@
import pickle
attraction_mapping = None
cinema_mapping = None
plays_mapping = None
excursion_mapping = None
concert_mapping = None
with open('passfinder/recomendations/service/mapping/attractions.pickle', 'rb') as file:
attraction_mapping = pickle.load(file)
with open('passfinder/recomendations/service/mapping/kino.pickle', 'rb') as file:
cinema_mapping = pickle.load(file)
with open('passfinder/recomendations/service/mapping/spektakli.pickle', 'rb') as file:
plays_mapping = pickle.load(file)
with open('passfinder/recomendations/service/mapping/excursii.pickle', 'rb') as file:
excursion_mapping = pickle.load(file)
with open('passfinder/recomendations/service/mapping/concerts.pickle', 'rb') as file:
concert_mapping = pickle.load(file)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
from annoy import AnnoyIndex
N_DIMENSIONAL = 768
attracion_model = AnnoyIndex(N_DIMENSIONAL, 'angular')
attracion_model.load('passfinder/recomendations/service/models/dost.ann')
cinema_model = AnnoyIndex(N_DIMENSIONAL, 'angular')
cinema_model.load('passfinder/recomendations/service/models/kino.ann')
plays_model = AnnoyIndex(N_DIMENSIONAL, 'angular')
plays_model.load('passfinder/recomendations/service/models/spektatli.ann')
excursion_model = AnnoyIndex(N_DIMENSIONAL, 'angular')
excursion_model.load('passfinder/recomendations/service/models/excursii.ann')
concert_model = AnnoyIndex(N_DIMENSIONAL, 'angular')
concert_model.load('passfinder/recomendations/service/models/concerts.ann')

Binary file not shown.

View File

@ -0,0 +1,39 @@
from annoy import AnnoyIndex
from .mapping.mapping import *
from .models.models import *
from passfinder.events.models import Event
def get_nearest_(instance_model, model_type, mapping, nearest_n, ml_model):
how_many = len(Event.objects.filter(type=model_type))
index = mapping.index(instance_model.oid)
nearest = ml_model.get_nns_by_item(index, len(mapping))
res = []
for i in range(how_many):
try:
res.append(Event.objects.get(oid=mapping[nearest[i]]))
except Event.DoesNotExist: ...
if len(res) == nearest_n: break
return res
def nearest_attraction(attraction, nearest_n):
return get_nearest_(attraction, 'attraction', attraction_mapping, nearest_n, attracion_model)
def nearest_movie(movie, nearest_n):
return get_nearest_(movie, 'movie', cinema_mapping, nearest_n, cinema_model)
def nearest_plays(play, nearest_n):
return get_nearest_(play, 'play', plays_mapping, nearest_n, plays_model)
def nearest_excursion(excursion, nearest_n):
return get_nearest_(excursion, 'excursion', excursion_mapping, nearest_n, excursion_model)
def nearest_concert(concert, nearest_n):
return get_nearest_(concert, 'concert', concert_mapping, nearest_n, concert_model)

View File

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

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -3,7 +3,7 @@
from django.db.models.enums import ChoicesMeta from django.db.models.enums import ChoicesMeta
def count_max_length(choices: Iterable | ChoicesMeta): def count_max_length(choices: any):
if isinstance(choices, ChoicesMeta): if isinstance(choices, ChoicesMeta):
return max([len(val) for val in choices.values]) return max([len(val) for val in choices.values])
return max([len(val) for val, _ in choices]) return max([len(val) for val, _ in choices])

2
poetry.lock generated
View File

@ -2805,5 +2805,5 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.8"
content-hash = "f28dac962f7703c39a58bd0f7640a1cc37af3dccd44f124e57e71d19d85278c0" content-hash = "f28dac962f7703c39a58bd0f7640a1cc37af3dccd44f124e57e71d19d85278c0"

View File

@ -6,7 +6,7 @@ authors = ["Alexandr Karpov <alexandr.d.karpov@gmail.com>"]
readme = "README.md" readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.8"
psycopg2 = "^2.9.5" psycopg2 = "^2.9.5"
pytz = "^2022.7" pytz = "^2022.7"
psutil = "^5.9.4" psutil = "^5.9.4"

134
requirements.txt Normal file
View File

@ -0,0 +1,134 @@
amqp==5.1.1 ; python_version >= "3.9" and python_version < "4.0"
anyio==3.6.2 ; python_version >= "3.9" and python_version < "4.0"
appnope==0.1.3 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "darwin"
argon2-cffi-bindings==21.2.0 ; python_version >= "3.9" and python_version < "4.0"
argon2-cffi==21.3.0 ; python_version >= "3.9" and python_version < "4.0"
asgiref==3.6.0 ; python_version >= "3.9" and python_version < "4.0"
astroid==2.15.5 ; python_version >= "3.9" and python_version < "4.0"
asttokens==2.2.1 ; python_version >= "3.9" and python_version < "4.0"
async-timeout==4.0.2 ; python_version >= "3.9" and python_full_version <= "3.9"
attrs==23.1.0 ; python_version >= "3.9" and python_version < "4.0"
backcall==0.2.0 ; python_version >= "3.9" and python_version < "4.0"
billiard==3.6.4.0 ; python_version >= "3.9" and python_version < "4.0"
black==22.12.0 ; python_version >= "3.9" and python_version < "4.0"
celery==5.2.7 ; python_version >= "3.9" and python_version < "4.0"
celery[redis]==5.2.7 ; python_version >= "3.9" and python_version < "4.0"
certifi==2023.5.7 ; python_version >= "3.9" and python_version < "4.0"
cffi==1.15.1 ; python_version >= "3.9" and python_version < "4.0"
cfgv==3.3.1 ; python_version >= "3.9" and python_version < "4.0"
charset-normalizer==3.1.0 ; python_version >= "3.9" and python_version < "4.0"
click-didyoumean==0.3.0 ; python_version >= "3.9" and python_version < "4.0"
click-plugins==1.1.1 ; python_version >= "3.9" and python_version < "4.0"
click-repl==0.2.0 ; python_version >= "3.9" and python_version < "4.0"
click==8.1.3 ; python_version >= "3.9" and python_version < "4.0"
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.9" and python_version < "4.0" and platform_system == "Windows"
coverage==7.2.5 ; python_version >= "3.9" and python_version < "4.0"
cron-descriptor==1.3.0 ; python_version >= "3.9" and python_version < "4.0"
decorator==5.1.1 ; python_version >= "3.9" and python_version < "4.0"
dill==0.3.6 ; python_version >= "3.9" and python_version < "4.0"
distlib==0.3.6 ; python_version >= "3.9" and python_version < "4.0"
django-celery-beat==2.5.0 ; python_version >= "3.9" and python_version < "4.0"
django-cors-headers==3.14.0 ; python_version >= "3.9" and python_version < "4.0"
django-coverage-plugin==3.0.0 ; python_version >= "3.9" and python_version < "4.0"
django-debug-toolbar==3.8.1 ; python_version >= "3.9" and python_version < "4.0"
django-environ==0.9.0 ; python_version >= "3.9" and python_version < "4"
django-extensions==3.2.1 ; python_version >= "3.9" and python_version < "4.0"
django-ipware==5.0.0 ; python_version >= "3.9" and python_version < "4.0"
django-location-field==2.7.0 ; python_version >= "3.9" and python_version < "4.0"
django-model-utils==4.3.1 ; python_version >= "3.9" and python_version < "4.0"
django-polymorphic==3.1.0 ; python_version >= "3.9" and python_version < "4.0"
django-redis==5.2.0 ; python_version >= "3.9" and python_version < "4.0"
django-structlog==4.1.1 ; python_version >= "3.9" and python_version < "4.0"
django-stubs-ext==4.2.0 ; python_version >= "3.9" and python_version < "4.0"
django-stubs==1.16.0 ; python_version >= "3.9" and python_version < "4.0"
django-timezone-field==5.0 ; python_version >= "3.9" and python_version < "4.0"
django==4.2.1 ; python_version >= "3.9" and python_version < "4.0"
djangorestframework-stubs==1.10.0 ; python_version >= "3.9" and python_version < "4.0"
djangorestframework==3.14.0 ; python_version >= "3.9" and python_version < "4.0"
drf-spectacular==0.25.1 ; python_version >= "3.9" and python_version < "4.0"
executing==1.2.0 ; python_version >= "3.9" and python_version < "4.0"
factory-boy==3.2.1 ; python_version >= "3.9" and python_version < "4.0"
faker==18.9.0 ; python_version >= "3.9" and python_version < "4.0"
filelock==3.12.0 ; python_version >= "3.9" and python_version < "4.0"
flake8-isort==6.0.0 ; python_version >= "3.9" and python_version < "4.0"
flake8==6.0.0 ; python_version >= "3.9" and python_version < "4.0"
flower==1.2.0 ; python_version >= "3.9" and python_version < "4.0"
humanize==4.6.0 ; python_version >= "3.9" and python_version < "4.0"
identify==2.5.24 ; python_version >= "3.9" and python_version < "4.0"
idna==3.4 ; python_version >= "3.9" and python_version < "4.0"
inflection==0.5.1 ; python_version >= "3.9" and python_version < "4.0"
iniconfig==2.0.0 ; python_version >= "3.9" and python_version < "4.0"
ipdb==0.13.13 ; python_version >= "3.9" and python_version < "4.0"
ipython==8.13.2 ; python_version >= "3.9" and python_version < "4.0"
isort==5.12.0 ; python_version >= "3.9" and python_version < "4.0"
jedi==0.18.2 ; python_version >= "3.9" and python_version < "4.0"
jsonschema==4.17.3 ; python_version >= "3.9" and python_version < "4.0"
kombu==5.2.4 ; python_version >= "3.9" and python_version < "4.0"
lazy-object-proxy==1.9.0 ; python_version >= "3.9" and python_version < "4.0"
markupsafe==2.1.2 ; python_version >= "3.9" and python_version < "4.0"
matplotlib-inline==0.1.6 ; python_version >= "3.9" and python_version < "4.0"
mccabe==0.7.0 ; python_version >= "3.9" and python_version < "4.0"
mypy-extensions==1.0.0 ; python_version >= "3.9" and python_version < "4.0"
mypy==0.991 ; python_version >= "3.9" and python_version < "4.0"
nodeenv==1.8.0 ; python_version >= "3.9" and python_version < "4.0"
packaging==23.1 ; python_version >= "3.9" and python_version < "4.0"
parso==0.8.3 ; python_version >= "3.9" and python_version < "4.0"
pexpect==4.8.0 ; python_version >= "3.9" and python_version < "4.0" and sys_platform != "win32"
pickleshare==0.7.5 ; python_version >= "3.9" and python_version < "4.0"
pillow==9.5.0 ; python_version >= "3.9" and python_version < "4.0"
platformdirs==3.5.1 ; python_version >= "3.9" and python_version < "4.0"
pluggy==1.0.0 ; python_version >= "3.9" and python_version < "4.0"
pre-commit==2.21.0 ; python_version >= "3.9" and python_version < "4.0"
prometheus-client==0.16.0 ; python_version >= "3.9" and python_version < "4.0"
prompt-toolkit==3.0.38 ; python_version >= "3.9" and python_version < "4.0"
psutil==5.9.5 ; python_version >= "3.9" and python_version < "4.0"
psycopg2==2.9.6 ; python_version >= "3.9" and python_version < "4.0"
ptyprocess==0.7.0 ; python_version >= "3.9" and python_version < "4.0" and sys_platform != "win32"
pure-eval==0.2.2 ; python_version >= "3.9" and python_version < "4.0"
pycodestyle==2.10.0 ; python_version >= "3.9" and python_version < "4.0"
pycparser==2.21 ; python_version >= "3.9" and python_version < "4.0"
pyflakes==3.0.1 ; python_version >= "3.9" and python_version < "4.0"
pygments==2.15.1 ; python_version >= "3.9" and python_version < "4.0"
pylint-celery==0.3 ; python_version >= "3.9" and python_version < "4.0"
pylint-django==2.5.3 ; python_version >= "3.9" and python_version < "4.0"
pylint-plugin-utils==0.8.1 ; python_version >= "3.9" and python_version < "4.0"
pylint==2.17.4 ; python_version >= "3.9" and python_version < "4.0"
pyrsistent==0.19.3 ; python_version >= "3.9" and python_version < "4.0"
pytest-django==4.5.2 ; python_version >= "3.9" and python_version < "4.0"
pytest-sugar==0.9.7 ; python_version >= "3.9" and python_version < "4.0"
pytest==7.3.1 ; python_version >= "3.9" and python_version < "4.0"
python-crontab==2.7.1 ; python_version >= "3.9" and python_version < "4.0"
python-dateutil==2.8.2 ; python_version >= "3.9" and python_version < "4.0"
python-slugify==7.0.0 ; python_version >= "3.9" and python_version < "4.0"
pytz==2022.7.1 ; python_version >= "3.9" and python_version < "4.0"
pyyaml==6.0 ; python_version >= "3.9" and python_version < "4.0"
redis==4.5.5 ; python_version >= "3.9" and python_version < "4.0"
requests==2.30.0 ; python_version >= "3.9" and python_version < "4.0"
sentry-sdk==1.23.1 ; python_version >= "3.9" and python_version < "4.0"
setuptools==67.7.2 ; python_version >= "3.9" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.9" and python_version < "4.0"
sniffio==1.3.0 ; python_version >= "3.9" and python_version < "4.0"
sqlparse==0.4.4 ; python_version >= "3.9" and python_version < "4.0"
stack-data==0.6.2 ; python_version >= "3.9" and python_version < "4.0"
structlog==23.1.0 ; python_version >= "3.9" and python_version < "4.0"
termcolor==2.3.0 ; python_version >= "3.9" and python_version < "4.0"
text-unidecode==1.3 ; python_version >= "3.9" and python_version < "4.0"
tomli==2.0.1 ; python_version >= "3.9" and python_version < "4.0"
tornado==6.3.2 ; python_version >= "3.9" and python_version < "4.0"
traitlets==5.9.0 ; python_version >= "3.9" and python_version < "4.0"
types-pytz==2023.3.0.0 ; python_version >= "3.9" and python_version < "4.0"
types-pyyaml==6.0.12.9 ; python_version >= "3.9" and python_version < "4.0"
types-requests==2.30.0.0 ; python_version >= "3.9" and python_version < "4.0"
types-urllib3==1.26.25.13 ; python_version >= "3.9" and python_version < "4.0"
typing-extensions==4.5.0 ; python_version >= "3.9" and python_version < "4.0"
tzdata==2023.3 ; python_version >= "3.9" and python_version < "4.0"
uritemplate==4.1.1 ; python_version >= "3.9" and python_version < "4.0"
urllib3==1.26.15 ; python_version >= "3.9" and python_version < "4.0"
vine==5.0.0 ; python_version >= "3.9" and python_version < "4.0"
virtualenv==20.23.0 ; python_version >= "3.9" and python_version < "4.0"
watchdog==3.0.0 ; python_version >= "3.9" and python_version < "4.0"
watchfiles==0.18.1 ; python_version >= "3.9" and python_version < "4.0"
wcwidth==0.2.6 ; python_version >= "3.9" and python_version < "4.0"
werkzeug[watchdog]==2.3.4 ; python_version >= "3.9" and python_version < "4.0"
whitenoise==6.4.0 ; python_version >= "3.9" and python_version < "4.0"
wrapt==1.15.0 ; python_version >= "3.9" and python_version < "4.0"