made things prettier

This commit is contained in:
2026-02-22 01:16:20 +01:00
parent 076293f80d
commit 7564da1148
10 changed files with 388 additions and 626 deletions

View File

@@ -1,5 +1,7 @@
import os import os
import logging import logging
import markdown
from markupsafe import Markup
from logging.handlers import WatchedFileHandler from logging.handlers import WatchedFileHandler
from flask import Flask from flask import Flask
from flask_socketio import SocketIO from flask_socketio import SocketIO
@@ -49,4 +51,10 @@ def create_app(config_class=Config):
app.register_blueprint(slides_bp) app.register_blueprint(slides_bp)
app.register_blueprint(admin_bp) app.register_blueprint(admin_bp)
@app.template_filter('md')
def render_markdown(text):
if not text:
return ''
return Markup(markdown.markdown(text, extensions=['nl2br']))
return app return app

744
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@
"license": "ISC", "license": "ISC",
"type": "commonjs", "type": "commonjs",
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"flowbite": "^3.1.2", "flowbite": "^3.1.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",

View File

@@ -8,82 +8,178 @@ bidict==0.23.1
# via python-socketio # via python-socketio
blinker==1.9.0 blinker==1.9.0
# via flask # via flask
brotli==1.2.0
# via geventhttpclient
cachetools==6.2.6 ; python_full_version < '3.10'
# via tutor-tool
cachetools==7.0.1 ; python_full_version >= '3.10'
# via tutor-tool
certifi==2026.1.4
# via
# geventhttpclient
# requests
cffi==2.0.0 ; (implementation_name == 'pypy' and platform_python_implementation != 'CPython') or (implementation_name == 'pypy' and sys_platform != 'win32') or (platform_python_implementation == 'CPython' and sys_platform == 'win32')
# via
# gevent
# pyzmq
charset-normalizer==3.4.4
# via requests
click==8.1.8 ; python_full_version < '3.10' click==8.1.8 ; python_full_version < '3.10'
# via flask # via flask
click==8.2.1 ; python_full_version >= '3.10' click==8.2.1 ; python_full_version >= '3.10'
# via flask # via flask
colorama==0.4.6 ; sys_platform == 'win32' colorama==0.4.6 ; sys_platform == 'win32'
# via click # via
# click
# pytest
configargparse==1.7.1
# via locust
dnspython==2.7.0 ; python_full_version < '3.10' dnspython==2.7.0 ; python_full_version < '3.10'
# via eventlet # via eventlet
dnspython==2.8.0 ; python_full_version >= '3.10' dnspython==2.8.0 ; python_full_version >= '3.10'
# via eventlet # via eventlet
eventlet==0.40.3 eventlet==0.40.3
# via tutor-tool # via tutor-tool
exceptiongroup==1.3.1 ; python_full_version == '3.10.*'
# via pytest
flask==3.1.2 flask==3.1.2
# via # via
# flask-bcrypt # flask-bcrypt
# flask-cors
# flask-login # flask-login
# flask-socketio # flask-socketio
# flask-sqlalchemy # flask-sqlalchemy
# flask-wtf # flask-wtf
# locust
# tutor-tool # tutor-tool
flask-bcrypt==1.0.1 flask-bcrypt==1.0.1
# via tutor-tool # via tutor-tool
flask-cors==6.0.2
# via locust
flask-login==0.6.3 flask-login==0.6.3
# via tutor-tool # via
# locust
# tutor-tool
flask-socketio==5.5.1 flask-socketio==5.5.1
# via tutor-tool # via tutor-tool
flask-sqlalchemy==3.1.1 flask-sqlalchemy==3.1.1
# via tutor-tool # via tutor-tool
flask-wtf==1.2.2 flask-wtf==1.2.2
# via tutor-tool # via tutor-tool
gevent==25.9.1
# via
# geventhttpclient
# locust
geventhttpclient==2.3.7
# via locust
greenlet==3.2.4 greenlet==3.2.4
# via # via
# eventlet # eventlet
# gevent
# sqlalchemy # sqlalchemy
gunicorn==23.0.0 gunicorn==23.0.0
# via tutor-tool # via tutor-tool
h11==0.16.0 h11==0.16.0
# via wsproto # via wsproto
idna==3.11
# via requests
importlib-metadata==8.7.0 ; python_full_version < '3.10' importlib-metadata==8.7.0 ; python_full_version < '3.10'
# via flask # via
# flask
# markdown
iniconfig==2.3.0 ; python_full_version >= '3.10'
# via pytest
itsdangerous==2.2.0 itsdangerous==2.2.0
# via # via
# flask # flask
# flask-wtf # flask-wtf
jinja2==3.1.6 jinja2==3.1.6
# via flask # via flask
locust==2.34.0 ; python_full_version < '3.10'
# via tutor-tool
locust==2.43.3 ; python_full_version >= '3.10'
# via tutor-tool
markdown==3.9 ; python_full_version < '3.10'
# via tutor-tool
markdown==3.10.2 ; python_full_version >= '3.10'
# via tutor-tool
markupsafe==3.0.2 markupsafe==3.0.2
# via # via
# flask # flask
# jinja2 # jinja2
# werkzeug # werkzeug
# wtforms # wtforms
msgpack==1.1.2
# via locust
packaging==25.0 packaging==25.0
# via gunicorn # via
# gunicorn
# pytest
pluggy==1.6.0 ; python_full_version >= '3.10'
# via pytest
psutil==7.2.2
# via locust
psycopg2-binary==2.9.11 psycopg2-binary==2.9.11
# via tutor-tool # via tutor-tool
pycparser==2.23 ; (python_full_version < '3.10' and implementation_name == 'pypy' and platform_python_implementation != 'CPython') or (python_full_version < '3.10' and implementation_name != 'PyPy' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (python_full_version < '3.10' and implementation_name == 'pypy' and sys_platform != 'win32')
# via cffi
pycparser==3.0 ; (python_full_version >= '3.10' and implementation_name == 'pypy' and platform_python_implementation != 'CPython') or (python_full_version >= '3.10' and implementation_name != 'PyPy' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (python_full_version >= '3.10' and implementation_name == 'pypy' and sys_platform != 'win32')
# via cffi
pygments==2.19.2 ; python_full_version >= '3.10'
# via pytest
pytest==9.0.2 ; python_full_version >= '3.10'
# via locust
python-engineio==4.12.2 python-engineio==4.12.2
# via python-socketio # via
# locust
# python-socketio
python-socketio==5.13.0 python-socketio==5.13.0
# via flask-socketio # via
# flask-socketio
# locust
pytz==2025.2 pytz==2025.2
# via tutor-tool # via tutor-tool
pywin32==311 ; sys_platform == 'win32'
# via locust
pyzmq==27.1.0
# via locust
redis==7.0.1 ; python_full_version < '3.10' redis==7.0.1 ; python_full_version < '3.10'
# via tutor-tool # via tutor-tool
redis==7.2.0 ; python_full_version >= '3.10' redis==7.2.0 ; python_full_version >= '3.10'
# via tutor-tool # via tutor-tool
requests==2.32.5
# via
# locust
# python-socketio
setuptools==82.0.0 ; python_full_version < '3.10'
# via
# locust
# zope-event
simple-websocket==1.1.0 simple-websocket==1.1.0
# via python-engineio # via python-engineio
sqlalchemy==2.0.43 sqlalchemy==2.0.43
# via flask-sqlalchemy # via flask-sqlalchemy
tomli==2.4.0 ; python_full_version < '3.11'
# via
# locust
# pytest
typing-extensions==4.15.0 typing-extensions==4.15.0
# via sqlalchemy # via
# exceptiongroup
# locust
# sqlalchemy
urllib3==2.6.3
# via
# geventhttpclient
# requests
websocket-client==1.9.0 ; python_full_version >= '3.10'
# via python-socketio
werkzeug==3.1.3 werkzeug==3.1.3
# via # via
# flask # flask
# flask-cors
# flask-login # flask-login
# locust
wsproto==1.2.0 wsproto==1.2.0
# via simple-websocket # via simple-websocket
wtforms==3.2.1 wtforms==3.2.1
@@ -92,3 +188,11 @@ xkcdpass==1.20.0
# via tutor-tool # via tutor-tool
zipp==3.23.0 ; python_full_version < '3.10' zipp==3.23.0 ; python_full_version < '3.10'
# via importlib-metadata # via importlib-metadata
zope-event==6.0 ; python_full_version < '3.10'
# via gevent
zope-event==6.1 ; python_full_version >= '3.10'
# via gevent
zope-interface==8.0.1 ; python_full_version < '3.10'
# via gevent
zope-interface==8.2 ; python_full_version >= '3.10'
# via gevent

View File

@@ -15,6 +15,7 @@ module.exports = {
}, },
}, },
plugins: [ plugins: [
require('flowbite/plugin') require('flowbite/plugin'),
require('@tailwindcss/typography')
], ],
} }

View File

@@ -11,14 +11,14 @@
{% block body %} {% block body %}
{# ── Header ────────────────────────────────────────────────────── #} {# ── Header ────────────────────────────────────────────────────── #}
<header class="bg-surface border-b border-border shadow-sm"> <header class="w-full px-4 pt-4 pb-2 md:px-6 md:py-3">
<div class="w-full px-6 py-3 flex items-center justify-between gap-4">
{# Left: user + logout #} {# ── Desktop: single row ───────────────────────────── #}
<div class="flex items-center gap-2 min-w-0"> <div class="hidden md:grid md:grid-cols-3 items-center gap-4">
<span class="text-gray-700 dark:text-gray-200 text-sm font-medium truncate">
{{ user_name }} {# Col 1: user + logout (left-aligned) #}
</span> <div class="flex items-center gap-2">
<span class="text-gray-700 dark:text-gray-200 text-sm font-medium">{{ user_name }}</span>
<form method="POST" action="/logout"> <form method="POST" action="/logout">
<button type="submit" <button type="submit"
class="text-xs text-white bg-red-600 px-2.5 py-1 rounded-md hover:bg-red-700 transition"> class="text-xs text-white bg-red-600 px-2.5 py-1 rounded-md hover:bg-red-700 transition">
@@ -27,12 +27,40 @@
</form> </form>
</div> </div>
{# Center: course switcher #} {# Col 2: course switcher (perfectly centered) #}
<div class="flex justify-center"> <div class="flex justify-center">
{{ course_switcher(courses, active_course_name, '/switch_course') }} {{ course_switcher(courses, active_course_name, '/switch_course') }}
</div> </div>
{# Right: social links + admin #} {# Col 3: social + admin (right-aligned) #}
<div class="flex items-center justify-end gap-3">
<a href="https://zulip.in.tum.de/#narrow/channel/3234-ITSec-Tutorium-Simon-Bu.C3.9Fmann" target="_blank">
<img src="{{ url_for('static', filename='zulip_icon.svg') }}" class="h-7 w-7">
</a>
<a href="https://discordapp.com/users/306086176585154560" target="_blank">
<img src="{{ url_for('static', filename='discord_icon.svg') }}" class="h-7 w-7">
</a>
<a href="{{ url_for('admin.admin') }}">
<button class="bg-primary hover:bg-blue-700 text-white text-sm px-3 py-1 rounded-md transition">
Admin
</button>
</a>
</div>
</div>
{# ── Mobile: single row only ──────────────────────── #}
<div class="flex flex-col gap-3 md:hidden">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-gray-700 dark:text-gray-200 text-sm font-medium">{{ user_name }}</span>
<form method="POST" action="/logout">
<button type="submit"
class="text-xs text-white bg-red-600 px-2.5 py-1 rounded-md hover:bg-red-700 transition">
Logout
</button>
</form>
</div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<a href="https://zulip.in.tum.de/#narrow/channel/3234-ITSec-Tutorium-Simon-Bu.C3.9Fmann" target="_blank"> <a href="https://zulip.in.tum.de/#narrow/channel/3234-ITSec-Tutorium-Simon-Bu.C3.9Fmann" target="_blank">
<img src="{{ url_for('static', filename='zulip_icon.svg') }}" class="h-7 w-7"> <img src="{{ url_for('static', filename='zulip_icon.svg') }}" class="h-7 w-7">
@@ -47,6 +75,8 @@
</a> </a>
</div> </div>
</div> </div>
</div>
</header> </header>
{# ── Main Content ──────────────────────────────────────────────── #} {# ── Main Content ──────────────────────────────────────────────── #}
@@ -60,6 +90,9 @@
</section> </section>
{% endif %} {% endif %}
<div class="flex justify-center md:hidden">
{{ course_switcher(courses, active_course_name, '/switch_course') }}
</div>
{# ── Password Form ─────────────────────────────────────────── #} {# ── Password Form ─────────────────────────────────────────── #}
<section class="flex justify-center"> <section class="flex justify-center">
@@ -110,7 +143,9 @@
<a href="/slides/{{ deck.course.folder }}/{{ deck.deck }}/{{ deck.index_file }}" <a href="/slides/{{ deck.course.folder }}/{{ deck.deck }}/{{ deck.index_file }}"
class="flex-grow block hover:text-primary transition"> class="flex-grow block hover:text-primary transition">
<h3 class="text-lg font-semibold mb-1">{{ deck.name }}</h3> <h3 class="text-lg font-semibold mb-1">{{ deck.name }}</h3>
<p class="text-muted text-sm">{{ deck.description }}</p> <div class="prose prose-sm dark:prose-invert prose-p:text-muted prose-li:text-muted">
{{ deck.description | md }}
</div>
</a> </a>
{% if deck.export_file %} {% if deck.export_file %}

View File

@@ -81,8 +81,11 @@
<button <button
type="button" type="button"
onclick="toggleCourseSwitcher()" onclick="toggleCourseSwitcher()"
class="flex items-center gap-2 px-4 py-1.5 rounded-lg border border-border bg-surface class="flex items-center gap-2 px-4 py-1.5 rounded-full
text-sm font-medium text-text hover:border-primary hover:text-primary transition whitespace-nowrap"> bg-surface shadow-md ring-1 ring-border
text-sm font-medium text-text
hover:ring-primary hover:text-primary
transition whitespace-nowrap">
<span id="course-switcher-label">{{ active_course_name }}</span> <span id="course-switcher-label">{{ active_course_name }}</span>
<svg class="w-3.5 h-3.5 text-muted transition-transform duration-200" id="course-switcher-chevron" <svg class="w-3.5 h-3.5 text-muted transition-transform duration-200" id="course-switcher-chevron"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6"> xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">

View File

@@ -1,11 +1,12 @@
import os import os
import logging import logging
from cachetools import TTLCache, cached
from eventlet.semaphore import Semaphore
from flask import request, session, redirect, url_for, current_app, abort, make_response from flask import request, session, redirect, url_for, current_app, abort, make_response
from typing import cast from typing import cast
from itsdangerous import BadSignature, URLSafeSerializer from itsdangerous import BadSignature, URLSafeSerializer
from xkcdpass import xkcd_password from xkcdpass import xkcd_password
from functools import wraps, lru_cache from functools import wraps
from urllib.parse import urlparse, urljoin from urllib.parse import urlparse, urljoin
from .models import Deck, InfoBanner, Token, db, Course from .models import Deck, InfoBanner, Token, db, Course
@@ -13,7 +14,8 @@ from .models import Deck, InfoBanner, Token, db, Course
# Create a module-level logger per Flask/Python standards # Create a module-level logger per Flask/Python standards
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_deck_cache = TTLCache(maxsize=1000, ttl=10)
_deck_cache_lock = Semaphore()
def init_password_generator(): def init_password_generator():
@@ -190,12 +192,14 @@ def token_required(f):
return decorated_function return decorated_function
# Cache the last 1000 lookups per worker.
@lru_cache(maxsize=1000)
@cached(_deck_cache, lock=_deck_cache_lock)
def get_cached_deck_info(token_id, course_folder, deck_slug): def get_cached_deck_info(token_id, course_folder, deck_slug):
""" """
Returns a dictionary of primitive types for route authorization and routing. Returns a dictionary of primitive types for route authorization and routing.
Never return raw SQLAlchemy models from an lru_cache! Never return raw SQLAlchemy models from a cache!
""" """
# 1. Fetch Token # 1. Fetch Token
token = db.session.get(Token, token_id) token = db.session.get(Token, token_id)
@@ -221,7 +225,6 @@ def get_cached_deck_info(token_id, course_folder, deck_slug):
# 4. Check Authorization # 4. Check Authorization
is_unlocked = deck_slug in [d.deck for d in token.decks] is_unlocked = deck_slug in [d.deck for d in token.decks]
# Return only primitive data (bools, strings) so the cache is thread-safe!
return { return {
"auth": is_unlocked, "auth": is_unlocked,
"course_exists": True, "course_exists": True,

View File

@@ -5,6 +5,7 @@ description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
"cachetools>=6.2.6",
"eventlet>=0.40.0", "eventlet>=0.40.0",
"flask>=3.1.1", "flask>=3.1.1",
"flask-bcrypt>=1.0.1", "flask-bcrypt>=1.0.1",
@@ -14,6 +15,7 @@ dependencies = [
"flask-wtf>=1.2.2", "flask-wtf>=1.2.2",
"gunicorn>=23.0.0", "gunicorn>=23.0.0",
"locust>=2.34.0", "locust>=2.34.0",
"markdown>=3.9",
"psycopg2-binary>=2.9.11", "psycopg2-binary>=2.9.11",
"pytz>=2025.2", "pytz>=2025.2",
"redis>=7.0.1", "redis>=7.0.1",

57
uv.lock generated
View File

@@ -171,6 +171,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/df/e7c780e463ee7bd7951770692bbea5a605f56b9809ec7f6ce751d7b2ee88/brotli-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937", size = 369008, upload-time = "2025-11-05T18:39:41.515Z" }, { url = "https://files.pythonhosted.org/packages/1b/df/e7c780e463ee7bd7951770692bbea5a605f56b9809ec7f6ce751d7b2ee88/brotli-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937", size = 369008, upload-time = "2025-11-05T18:39:41.515Z" },
] ]
[[package]]
name = "cachetools"
version = "6.2.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" },
]
[[package]]
name = "cachetools"
version = "7.0.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.1.4" version = "2026.1.4"
@@ -930,6 +954,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/d2/dc5379876d3a481720803653ea4d219f0c26f2d2b37c9243baaa16d0bc79/locust-2.43.3-py3-none-any.whl", hash = "sha256:e032c119b54a9d984cb74a936ee83cfd7d68b3c76c8f308af63d04f11396b553", size = 1463473, upload-time = "2026-02-12T09:55:31.727Z" }, { url = "https://files.pythonhosted.org/packages/3d/d2/dc5379876d3a481720803653ea4d219f0c26f2d2b37c9243baaa16d0bc79/locust-2.43.3-py3-none-any.whl", hash = "sha256:e032c119b54a9d984cb74a936ee83cfd7d68b3c76c8f308af63d04f11396b553", size = 1463473, upload-time = "2026-02-12T09:55:31.727Z" },
] ]
[[package]]
name = "markdown"
version = "3.9"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.10'",
]
dependencies = [
{ name = "importlib-metadata", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" },
]
[[package]]
name = "markdown"
version = "3.10.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.10'",
]
sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" },
]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "3.0.2" version = "3.0.2"
@@ -1569,6 +1620,8 @@ name = "tutor-tool"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "cachetools", version = "6.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "cachetools", version = "7.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "eventlet" }, { name = "eventlet" },
{ name = "flask" }, { name = "flask" },
{ name = "flask-bcrypt" }, { name = "flask-bcrypt" },
@@ -1579,6 +1632,8 @@ dependencies = [
{ name = "gunicorn" }, { name = "gunicorn" },
{ name = "locust", version = "2.34.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "locust", version = "2.34.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "locust", version = "2.43.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "locust", version = "2.43.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "pytz" }, { name = "pytz" },
{ name = "redis", version = "7.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "redis", version = "7.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
@@ -1588,6 +1643,7 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "cachetools", specifier = ">=6.2.6" },
{ name = "eventlet", specifier = ">=0.40.0" }, { name = "eventlet", specifier = ">=0.40.0" },
{ name = "flask", specifier = ">=3.1.1" }, { name = "flask", specifier = ">=3.1.1" },
{ name = "flask-bcrypt", specifier = ">=1.0.1" }, { name = "flask-bcrypt", specifier = ">=1.0.1" },
@@ -1597,6 +1653,7 @@ requires-dist = [
{ name = "flask-wtf", specifier = ">=1.2.2" }, { name = "flask-wtf", specifier = ">=1.2.2" },
{ name = "gunicorn", specifier = ">=23.0.0" }, { name = "gunicorn", specifier = ">=23.0.0" },
{ name = "locust", specifier = ">=2.34.0" }, { name = "locust", specifier = ">=2.34.0" },
{ name = "markdown", specifier = ">=3.9" },
{ name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "psycopg2-binary", specifier = ">=2.9.11" },
{ name = "pytz", specifier = ">=2025.2" }, { name = "pytz", specifier = ">=2025.2" },
{ name = "redis", specifier = ">=7.0.1" }, { name = "redis", specifier = ">=7.0.1" },