@@ -12,11 +16,11 @@
QR Code Generated
 }})
-
Download QR Code without Image
+
Download QR Code without Image
{% if qr_with_image_exists %}
 }})
-
Download QR Code with Image
+
Download QR Code with Image
{% endif %}
From 861cc8155b03b514827fa6b45b1e2fed6d6342e5 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 11:03:17 +0200
Subject: [PATCH 05/17] Upgrade to Bootstrap 5, drop jQuery, add tests, clean
up repo
---
.gitignore | 1 +
app.log | 213 ------------------
requirements.txt | 2 +
static/js/input_errors_modal.js | 55 +++--
templates/index.html | 22 +-
templates/result.html | 4 +-
tests/__init__.py | 0
tests/conftest.py | 43 ++++
tests/test_mixins.py | 93 ++++++++
tests/test_result_store.py | 54 +++++
tests/test_routes.py | 87 +++++++
utils/__pycache__/logger.cpython-312.pyc | Bin 1306 -> 0 bytes
utils/__pycache__/mixins.cpython-312.pyc | Bin 2285 -> 0 bytes
.../__pycache__/qr_generator.cpython-312.pyc | Bin 1450 -> 0 bytes
14 files changed, 316 insertions(+), 258 deletions(-)
delete mode 100644 app.log
create mode 100644 tests/__init__.py
create mode 100644 tests/conftest.py
create mode 100644 tests/test_mixins.py
create mode 100644 tests/test_result_store.py
create mode 100644 tests/test_routes.py
delete mode 100644 utils/__pycache__/logger.cpython-312.pyc
delete mode 100644 utils/__pycache__/mixins.cpython-312.pyc
delete mode 100644 utils/__pycache__/qr_generator.cpython-312.pyc
diff --git a/.gitignore b/.gitignore
index 3b0d7bc..675e6b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ app.log
__pycache__/
*.pyc
.venv/
+.pytest_cache/
diff --git a/app.log b/app.log
deleted file mode 100644
index 0680b63..0000000
--- a/app.log
+++ /dev/null
@@ -1,213 +0,0 @@
-2024-04-16 05:16:42,760 - INFO - [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
- * Running on http://127.0.0.1:5000
-2024-04-16 05:16:42,760 - INFO - [33mPress CTRL+C to quit[0m
-2024-04-16 05:16:42,761 - INFO - * Restarting with stat
-2024-04-16 05:16:42,947 - WARNING - * Debugger is active!
-2024-04-16 05:16:42,953 - INFO - * Debugger PIN: 136-546-380
-2024-04-16 05:17:13,010 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 05:17:13,013 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:13] "GET / HTTP/1.1" 200 -
-2024-04-16 05:17:13,146 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:13] "GET /static/js/form_remove_image.js HTTP/1.1" 200 -
-2024-04-16 05:17:13,147 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:13] "GET /static/js/input_errors_modal.js HTTP/1.1" 200 -
-2024-04-16 05:17:34,782 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:34] "GET / HTTP/1.1" 200 -
-2024-04-16 05:17:34,812 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:34] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:17:34,813 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:34] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:17:37,759 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:37] "GET / HTTP/1.1" 200 -
-2024-04-16 05:17:37,790 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:37] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:17:37,790 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:37] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:17:43,620 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:43] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:17:43,631 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 05:17:43,631 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:43] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:17:43,676 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:43] "[35m[1mGET /download HTTP/1.1[0m" 500 -
-2024-04-16 05:17:43,705 - INFO - 127.0.0.1 - - [16/Apr/2024 05:17:43] "[35m[1mGET /download_with_image HTTP/1.1[0m" 500 -
-2024-04-16 05:18:06,974 - INFO - 127.0.0.1 - - [16/Apr/2024 05:18:06] "GET / HTTP/1.1" 200 -
-2024-04-16 05:18:09,564 - INFO - 127.0.0.1 - - [16/Apr/2024 05:18:09] "GET / HTTP/1.1" 200 -
-2024-04-16 05:18:09,603 - INFO - 127.0.0.1 - - [16/Apr/2024 05:18:09] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:18:09,606 - INFO - 127.0.0.1 - - [16/Apr/2024 05:18:09] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:20:36,453 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\app.py', reloading
-2024-04-16 05:20:36,485 - INFO - * Restarting with stat
-2024-04-16 05:20:36,687 - WARNING - * Debugger is active!
-2024-04-16 05:20:36,691 - INFO - * Debugger PIN: 136-546-380
-2024-04-16 05:20:40,442 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:40] "GET / HTTP/1.1" 200 -
-2024-04-16 05:20:40,558 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:40] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:20:40,559 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:40] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:20:46,243 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:46] "GET / HTTP/1.1" 200 -
-2024-04-16 05:20:46,279 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:46] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:20:46,279 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:46] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:20:50,825 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:50] "GET / HTTP/1.1" 200 -
-2024-04-16 05:20:50,862 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:50] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:20:50,863 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:50] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:20:53,352 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:53] "GET / HTTP/1.1" 200 -
-2024-04-16 05:20:53,387 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:53] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:20:53,388 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:53] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:20:57,894 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:57] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:20:57,905 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:57] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:20:57,990 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:57] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:20:57,991 - INFO - 127.0.0.1 - - [16/Apr/2024 05:20:57] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:21:05,702 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 05:21:05,703 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:05] "GET / HTTP/1.1" 200 -
-2024-04-16 05:21:08,476 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:08] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:21:08,486 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:08] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:21:08,542 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:08] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:21:08,548 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:08] "[35m[1mGET /download_with_image HTTP/1.1[0m" 500 -
-2024-04-16 05:21:10,445 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "[35m[1mGET /download_with_image HTTP/1.1[0m" 500 -
-2024-04-16 05:21:10,495 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "GET /download_with_image?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
-2024-04-16 05:21:10,495 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "GET /download_with_image?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
-2024-04-16 05:21:10,530 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "GET /download_with_image?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
-2024-04-16 05:21:10,537 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:10] "[36mGET /download_with_image?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1[0m" 304 -
-2024-04-16 05:21:53,142 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:53] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:21:53,182 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:53] "[35m[1mGET /download_with_image HTTP/1.1[0m" 500 -
-2024-04-16 05:21:53,214 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:53] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:21:56,845 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:56] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:21:56,876 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:56] "[35m[1mGET /download_with_image HTTP/1.1[0m" 500 -
-2024-04-16 05:21:56,908 - INFO - 127.0.0.1 - - [16/Apr/2024 05:21:56] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:22:05,360 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:05] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:22:05,387 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:05] "[35m[1mGET /download_with_image HTTP/1.1[0m" 500 -
-2024-04-16 05:22:05,411 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:05] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:22:14,547 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:14] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:22:14,574 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:14] "[35m[1mGET /download_with_image HTTP/1.1[0m" 500 -
-2024-04-16 05:22:14,596 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:14] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:22:41,914 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:41] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:22:41,950 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:41] "[35m[1mGET /download_with_image HTTP/1.1[0m" 500 -
-2024-04-16 05:22:41,964 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:41] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:22:54,548 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:54] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:22:54,580 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:54] "[35m[1mGET /download_with_image HTTP/1.1[0m" 500 -
-2024-04-16 05:22:54,605 - INFO - 127.0.0.1 - - [16/Apr/2024 05:22:54] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:24:15,869 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:15] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:24:15,918 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:15] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:24:16,524 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:16] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:24:16,625 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:16] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:24:17,407 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:17] "GET / HTTP/1.1" 200 -
-2024-04-16 05:24:18,420 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:18] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:24:18,432 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:18] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:24:18,494 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:18] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:24:25,556 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:25] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:24:25,572 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:25] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:24:25,674 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:25] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:24:44,722 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:44] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:24:44,775 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:44] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:24:45,443 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:45] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:24:45,502 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:45] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:24:46,730 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:46] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:24:46,821 - INFO - 127.0.0.1 - - [16/Apr/2024 05:24:46] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:26:13,901 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\app.py', reloading
-2024-04-16 05:26:13,934 - INFO - * Restarting with stat
-2024-04-16 05:26:14,161 - WARNING - * Debugger is active!
-2024-04-16 05:26:14,166 - INFO - * Debugger PIN: 136-546-380
-2024-04-16 05:26:23,270 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\app.py', reloading
-2024-04-16 05:26:23,304 - INFO - * Restarting with stat
-2024-04-16 05:26:23,505 - WARNING - * Debugger is active!
-2024-04-16 05:26:23,510 - INFO - * Debugger PIN: 136-546-380
-2024-04-16 05:26:24,505 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:24] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:26:24,698 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:24] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:26:24,700 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:24] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:26:24,737 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:24] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
-2024-04-16 05:26:25,942 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:25] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:26:26,018 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:26] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:26:26,019 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:26] "[36mGET /download_with_image HTTP/1.1[0m" 304 -
-2024-04-16 05:26:42,772 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:42] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:26:42,825 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:42] "[36mGET /download HTTP/1.1[0m" 304 -
-2024-04-16 05:26:42,825 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:42] "[36mGET /download_with_image HTTP/1.1[0m" 304 -
-2024-04-16 05:26:47,846 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 05:26:47,848 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:47] "GET / HTTP/1.1" 200 -
-2024-04-16 05:26:47,878 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:47] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:26:47,878 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:47] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:26:52,480 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:52] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:26:52,491 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:52] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:26:52,562 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:52] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:26:54,115 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:54] "GET / HTTP/1.1" 200 -
-2024-04-16 05:26:57,509 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:57] "GET / HTTP/1.1" 200 -
-2024-04-16 05:26:57,534 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:57] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:26:57,535 - INFO - 127.0.0.1 - - [16/Apr/2024 05:26:57] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:28:58,363 - INFO - 127.0.0.1 - - [16/Apr/2024 05:28:58] "GET / HTTP/1.1" 200 -
-2024-04-16 05:28:58,412 - INFO - 127.0.0.1 - - [16/Apr/2024 05:28:58] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:28:58,416 - INFO - 127.0.0.1 - - [16/Apr/2024 05:28:58] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:29:33,501 - INFO - 127.0.0.1 - - [16/Apr/2024 05:29:33] "GET / HTTP/1.1" 200 -
-2024-04-16 05:29:33,536 - INFO - 127.0.0.1 - - [16/Apr/2024 05:29:33] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:29:33,536 - INFO - 127.0.0.1 - - [16/Apr/2024 05:29:33] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:30:06,705 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:06] "GET / HTTP/1.1" 200 -
-2024-04-16 05:30:06,742 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:06] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:30:06,743 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:06] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:30:07,622 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:07] "GET / HTTP/1.1" 200 -
-2024-04-16 05:30:07,691 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:07] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:30:07,693 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:07] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:30:30,254 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:30] "GET / HTTP/1.1" 200 -
-2024-04-16 05:30:30,297 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:30] "GET /static/js/input_errors_modal.js HTTP/1.1" 200 -
-2024-04-16 05:30:30,300 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:30] "GET /static/js/form_remove_image.js HTTP/1.1" 200 -
-2024-04-16 05:30:41,634 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:41] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:30:41,650 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:41] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:30:41,688 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:41] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:30:41,689 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:41] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:30:44,216 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 05:30:44,217 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:44] "GET / HTTP/1.1" 200 -
-2024-04-16 05:30:47,545 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:47] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:30:47,557 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:47] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:30:47,619 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:47] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:30:47,620 - INFO - 127.0.0.1 - - [16/Apr/2024 05:30:47] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:31:38,356 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\utils\\mixins.py', reloading
-2024-04-16 05:31:38,383 - INFO - * Restarting with stat
-2024-04-16 05:31:38,576 - WARNING - * Debugger is active!
-2024-04-16 05:31:38,581 - INFO - * Debugger PIN: 136-546-380
-2024-04-16 05:31:38,593 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 05:31:38,598 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:38] "GET / HTTP/1.1" 200 -
-2024-04-16 05:31:38,719 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:38] "GET /static/js/input_errors_modal.js HTTP/1.1" 200 -
-2024-04-16 05:31:38,720 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:38] "GET /static/js/form_remove_image.js HTTP/1.1" 200 -
-2024-04-16 05:31:39,357 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:39] "GET / HTTP/1.1" 200 -
-2024-04-16 05:31:39,405 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:39] "[36mGET /static/js/input_errors_modal.js HTTP/1.1[0m" 304 -
-2024-04-16 05:31:39,409 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:39] "[36mGET /static/js/form_remove_image.js HTTP/1.1[0m" 304 -
-2024-04-16 05:31:42,816 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:42] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:31:42,837 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:42] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:31:42,966 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:42] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:31:42,966 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:42] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:31:44,499 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 05:31:44,499 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:44] "GET / HTTP/1.1" 200 -
-2024-04-16 05:31:47,171 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:47] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:31:47,192 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:47] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:31:47,264 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:47] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:31:47,264 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:47] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:31:50,414 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:50] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:31:50,424 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:50] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:31:50,488 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:50] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:31:50,489 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:50] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:31:54,267 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:54] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:31:54,277 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:54] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:31:54,334 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:54] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:31:54,335 - INFO - 127.0.0.1 - - [16/Apr/2024 05:31:54] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:33:28,318 - INFO - 127.0.0.1 - - [16/Apr/2024 05:33:28] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:33:28,337 - INFO - 127.0.0.1 - - [16/Apr/2024 05:33:28] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:33:28,444 - INFO - 127.0.0.1 - - [16/Apr/2024 05:33:28] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:33:28,447 - INFO - 127.0.0.1 - - [16/Apr/2024 05:33:28] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:37:36,052 - INFO - 127.0.0.1 - - [16/Apr/2024 05:37:36] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:37:36,065 - INFO - 127.0.0.1 - - [16/Apr/2024 05:37:36] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:37:36,132 - INFO - 127.0.0.1 - - [16/Apr/2024 05:37:36] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:37:36,133 - INFO - 127.0.0.1 - - [16/Apr/2024 05:37:36] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:38:00,078 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:00] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:38:00,091 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:00] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:38:00,152 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:00] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:38:00,152 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:00] "GET /download_with_image HTTP/1.1" 200 -
-2024-04-16 05:38:07,088 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 05:38:07,088 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:07] "GET / HTTP/1.1" 200 -
-2024-04-16 05:38:09,111 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:09] "POST /generate HTTP/1.1" 200 -
-2024-04-16 05:38:09,125 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:09] "GET /result HTTP/1.1" 200 -
-2024-04-16 05:38:09,193 - INFO - 127.0.0.1 - - [16/Apr/2024 05:38:09] "GET /download HTTP/1.1" 200 -
-2024-04-16 05:39:24,775 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\utils\\qr_generator.py', reloading
-2024-04-16 05:39:24,812 - INFO - * Restarting with stat
-2024-04-16 05:39:25,031 - WARNING - * Debugger is active!
-2024-04-16 05:39:25,037 - INFO - * Debugger PIN: 136-546-380
-2024-04-16 05:41:26,769 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\utils\\logger.py', reloading
-2024-04-16 05:41:26,805 - INFO - * Restarting with stat
-2024-04-16 05:41:27,005 - WARNING - * Debugger is active!
-2024-04-16 05:41:27,010 - INFO - * Debugger PIN: 136-546-380
-2024-04-16 05:42:33,413 - INFO - * Detected change in 'H:\\Dev_Mentoring\\QR_generator\\app.py', reloading
-2024-04-16 05:42:33,448 - INFO - * Restarting with stat
-2024-04-16 05:55:20,539 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 06:11:04,905 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.gif'
-2024-04-16 06:11:19,712 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 06:13:39,656 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 06:13:55,412 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 06:19:41,117 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.gif'
-2024-04-16 06:26:39,508 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.gif'
-2024-04-16 06:26:45,748 - ERROR - Error removing file qr_with_image.png: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.png'
-2024-04-16 06:27:20,691 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.gif'
-2024-04-16 06:34:42,506 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.gif'
-2024-04-16 06:35:34,416 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.gif'
-2024-04-16 06:37:00,955 - ERROR - Error removing file qr_with_image.gif: [WinError 2] Nie można odnaleć okrelonego pliku: 'qr_with_image.gif'
diff --git a/requirements.txt b/requirements.txt
index da963ef..9080073 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,3 +10,5 @@ qrcode-artistic==3.0.2
segno==1.6.1
Werkzeug==3.0.2
Flask-WTF==1.2.1
+pytest
+pytest-flask
diff --git a/static/js/input_errors_modal.js b/static/js/input_errors_modal.js
index 997b905..42e015e 100644
--- a/static/js/input_errors_modal.js
+++ b/static/js/input_errors_modal.js
@@ -1,37 +1,34 @@
-$(document).ready(function(){
+document.addEventListener('DOMContentLoaded', function () {
+ var errorModal = new bootstrap.Modal(document.getElementById('errorModal'));
+
function showErrors(errors) {
- var $body = $('#errorModalBody').empty();
+ var body = document.getElementById('errorModalBody');
+ body.innerHTML = '';
(errors && errors.length ? errors : ['An unexpected error occurred.'])
- .forEach(function(error) { $('
').text(error).appendTo($body); });
- $('#errorModal').modal('show');
+ .forEach(function (error) {
+ var p = document.createElement('p');
+ p.textContent = error;
+ body.appendChild(p);
+ });
+ errorModal.show();
}
- $('#generate-form').submit(function(event){
+ document.getElementById('generate-form').addEventListener('submit', function (event) {
event.preventDefault();
- var formData = new FormData($(this)[0]);
-
- $.ajax({
- type: 'POST',
- url: '/generate',
- data: formData,
- processData: false,
- contentType: false,
- success: function(response) {
- if (response.success) {
- window.location.href = '/result?id=' + encodeURIComponent(response.id);
- } else {
- showErrors(response.errors);
- }
- },
- error: function(xhr) {
- console.error(xhr.responseText);
- var errors = (xhr.responseJSON && xhr.responseJSON.errors) ? xhr.responseJSON.errors : null;
- if (!errors && xhr.status === 413) {
- errors = ['File is too large. Maximum request size is 6 MB.'];
- }
- showErrors(errors);
- }
- });
+ fetch('/generate', { method: 'POST', body: new FormData(this) })
+ .then(function (response) {
+ return response.json().then(function (data) {
+ if (data.success) {
+ window.location.href = '/result?id=' + encodeURIComponent(data.id);
+ } else {
+ showErrors(data.errors);
+ }
+ });
+ })
+ .catch(function (err) {
+ console.error(err);
+ showErrors(null);
+ });
});
});
diff --git a/templates/index.html b/templates/index.html
index d6f89d3..9ace205 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -5,8 +5,8 @@
QR Code Generator
-
@@ -26,7 +26,7 @@
QR Code Generator
-
+
@@ -35,14 +35,12 @@
QR Code Generator
-
-
+
+
No errors
@@ -51,12 +49,8 @@
Error
-
-
-
diff --git a/templates/result.html b/templates/result.html
index 6ade6f4..342f563 100644
--- a/templates/result.html
+++ b/templates/result.html
@@ -5,8 +5,8 @@
QR Code Generated
-
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..c345c49
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,43 @@
+import base64
+import io
+
+import pytest
+
+from app import app as flask_app, store
+
+
+@pytest.fixture
+def app():
+ flask_app.config['TESTING'] = True
+ flask_app.config['WTF_CSRF_ENABLED'] = False
+ return flask_app
+
+
+@pytest.fixture
+def client(app):
+ return app.test_client()
+
+
+@pytest.fixture
+def png_bytes():
+ return base64.b64decode(
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12NgAAIABQ'
+ 'AABjkB6QAAAABJRU5ErkJggg=='
+ )
+
+
+@pytest.fixture
+def png_file(png_bytes):
+ return io.BytesIO(png_bytes)
+
+
+@pytest.fixture
+def qr_token():
+ token = store.put({'qr': b'fakeqr', 'artistic': None, 'artistic_ext': None})
+ return token
+
+
+@pytest.fixture
+def qr_with_artistic_token():
+ token = store.put({'qr': b'fakeqr', 'artistic': b'fakeart', 'artistic_ext': 'png'})
+ return token
diff --git a/tests/test_mixins.py b/tests/test_mixins.py
new file mode 100644
index 0000000..aa4807a
--- /dev/null
+++ b/tests/test_mixins.py
@@ -0,0 +1,93 @@
+import io
+from unittest.mock import MagicMock
+
+from utils.mixins import NotificationMixin, MIN_SIZE, MAX_SIZE
+
+
+class Validator(NotificationMixin):
+ pass
+
+
+v = Validator()
+
+
+def _mock_file(data: bytes, filename: str = 'image.png'):
+ f = MagicMock()
+ f.filename = filename
+ buf = io.BytesIO(data)
+ f.read.side_effect = buf.read
+ f.seek.side_effect = buf.seek
+ return f
+
+
+class TestValidateText:
+ def test_empty_string_is_valid(self):
+ ok, msg = v.validate_text('')
+ assert ok is True
+ assert msg == ''
+
+ def test_55_chars_is_valid(self):
+ ok, _ = v.validate_text('x' * 55)
+ assert ok is True
+
+ def test_56_chars_is_invalid(self):
+ ok, msg = v.validate_text('x' * 56)
+ assert ok is False
+ assert '55' in msg
+
+
+class TestValidateSize:
+ def test_min_boundary(self):
+ ok, _ = v.validate_size(MIN_SIZE)
+ assert ok is True
+
+ def test_max_boundary(self):
+ ok, _ = v.validate_size(MAX_SIZE)
+ assert ok is True
+
+ def test_below_min(self):
+ ok, msg = v.validate_size(MIN_SIZE - 1)
+ assert ok is False
+ assert str(MIN_SIZE) in msg
+ assert str(MAX_SIZE) in msg
+
+ def test_above_max(self):
+ ok, msg = v.validate_size(MAX_SIZE + 1)
+ assert ok is False
+ assert str(MAX_SIZE) in msg
+
+
+class TestValidateImage:
+ def test_none_is_valid(self):
+ ok, msg = v.validate_image(None)
+ assert ok is True
+ assert msg == ''
+
+ def test_disallowed_extension(self):
+ f = _mock_file(b'data', filename='image.bmp')
+ ok, msg = v.validate_image(f)
+ assert ok is False
+ assert 'PNG' in msg or 'format' in msg.lower()
+
+ def test_oversized_file(self):
+ large = b'x' * (5 * 1024 * 1024 + 1)
+ f = _mock_file(large, filename='image.png')
+ ok, msg = v.validate_image(f)
+ assert ok is False
+ assert '5' in msg
+
+ def test_seek_reset_after_size_check(self):
+ data = b'x' * 100
+ f = _mock_file(data, filename='image.png')
+ v.validate_image(f)
+ f.seek.assert_called_with(0)
+
+ def test_valid_png(self):
+ f = _mock_file(b'x' * 100, filename='image.png')
+ ok, _ = v.validate_image(f)
+ assert ok is True
+
+ def test_gif_extension_allowed(self):
+ f = _mock_file(b'x' * 100, filename='anim.gif')
+ ok, _ = v.validate_image(f)
+ assert ok is True
diff --git a/tests/test_result_store.py b/tests/test_result_store.py
new file mode 100644
index 0000000..005f0d6
--- /dev/null
+++ b/tests/test_result_store.py
@@ -0,0 +1,54 @@
+import time
+from unittest.mock import patch
+
+from utils.result_store import ResultStore
+
+
+def test_put_returns_string():
+ store = ResultStore()
+ token = store.put({'data': 1})
+ assert isinstance(token, str)
+ assert len(token) > 0
+
+
+def test_put_returns_unique_tokens():
+ store = ResultStore()
+ tokens = {store.put({'n': i}) for i in range(10)}
+ assert len(tokens) == 10
+
+
+def test_get_returns_payload():
+ store = ResultStore()
+ payload = {'qr': b'bytes', 'x': 42}
+ token = store.put(payload)
+ assert store.get(token) == payload
+
+
+def test_get_unknown_token_returns_none():
+ store = ResultStore()
+ assert store.get('nonexistent') is None
+
+
+def test_get_expired_entry_returns_none():
+ store = ResultStore(ttl=10)
+ token = store.put({'data': 1})
+ future = time.monotonic() + 11
+ with patch('utils.result_store.time.monotonic', return_value=future):
+ assert store.get(token) is None
+
+
+def test_fifo_eviction_at_cap():
+ store = ResultStore(max_entries=3)
+ t1 = store.put({'n': 1})
+ store.put({'n': 2})
+ store.put({'n': 3})
+ store.put({'n': 4})
+ assert store.get(t1) is None
+
+
+def test_no_duplicate_handler_on_reconfigure():
+ store = ResultStore(max_entries=2)
+ t1 = store.put({'n': 1})
+ t2 = store.put({'n': 2})
+ t3 = store.put({'n': 3})
+ assert store.get(t2) is not None or store.get(t3) is not None
diff --git a/tests/test_routes.py b/tests/test_routes.py
new file mode 100644
index 0000000..133e398
--- /dev/null
+++ b/tests/test_routes.py
@@ -0,0 +1,87 @@
+import io
+
+from app import store
+
+
+def test_index(client):
+ response = client.get('/')
+ assert response.status_code == 200
+
+
+class TestGenerate:
+ def test_valid_text_and_size(self, client):
+ response = client.post('/generate', data={'text': 'hello', 'size': '10'})
+ assert response.status_code == 200
+ data = response.get_json()
+ assert data['success'] is True
+ assert isinstance(data['id'], str)
+
+ def test_invalid_size_non_integer(self, client):
+ response = client.post('/generate', data={'text': 'hello', 'size': 'abc'})
+ assert response.status_code == 400
+ data = response.get_json()
+ assert data['success'] is False
+ assert len(data['errors']) > 0
+
+ def test_missing_size_field(self, client):
+ response = client.post('/generate', data={'text': 'hello'})
+ assert response.status_code == 400
+ data = response.get_json()
+ assert data['success'] is False
+
+ def test_text_too_long_returns_validation_error(self, client):
+ response = client.post('/generate', data={'text': 'x' * 56, 'size': '10'})
+ assert response.status_code == 200
+ data = response.get_json()
+ assert data['success'] is False
+ assert len(data['errors']) > 0
+
+ def test_size_out_of_range(self, client):
+ response = client.post('/generate', data={'text': 'hi', 'size': '31'})
+ assert response.status_code == 200
+ data = response.get_json()
+ assert data['success'] is False
+
+
+class TestResult:
+ def test_valid_token(self, client, qr_token):
+ response = client.get(f'/result?id={qr_token}')
+ assert response.status_code == 200
+
+ def test_invalid_token_returns_404(self, client):
+ response = client.get('/result?id=doesnotexist')
+ assert response.status_code == 404
+
+ def test_missing_id_returns_404(self, client):
+ response = client.get('/result')
+ assert response.status_code == 404
+
+
+class TestDownload:
+ def test_valid_token_returns_png(self, client, qr_token):
+ response = client.get(f'/download?id={qr_token}')
+ assert response.status_code == 200
+ assert response.content_type == 'image/png'
+
+ def test_invalid_token_returns_404(self, client):
+ response = client.get('/download?id=bad')
+ assert response.status_code == 404
+
+ def test_missing_id_returns_404(self, client):
+ response = client.get('/download')
+ assert response.status_code == 404
+
+
+class TestDownloadWithImage:
+ def test_token_without_artistic_returns_404(self, client, qr_token):
+ response = client.get(f'/download_with_image?id={qr_token}')
+ assert response.status_code == 404
+
+ def test_token_with_artistic_returns_200(self, client, qr_with_artistic_token):
+ response = client.get(f'/download_with_image?id={qr_with_artistic_token}')
+ assert response.status_code == 200
+ assert 'image/' in response.content_type
+
+ def test_invalid_token_returns_404(self, client):
+ response = client.get('/download_with_image?id=bad')
+ assert response.status_code == 404
diff --git a/utils/__pycache__/logger.cpython-312.pyc b/utils/__pycache__/logger.cpython-312.pyc
deleted file mode 100644
index 1b3895d8c5f703140125651a3c90ac0ae864ad3f..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1306
zcmah}%}*0S6rcTSXZZ#(g4M`Js%$KcXJbhGLJo@3i?OMhEZyl++;;2k7K9YTfddEc
zaO7(6peOwwywFes88tDHix;ER#Ke
ddaj^-3je*=7-;KJ-n3gZc1AJctu>WAm24i9OJPEVgbE?)e(h
zc);X`|pHwS1?P}Ru)shps
zoA-((s<|VRBgqMiE>WwjxA!ILy1EHoO=WTDnh7(*RFir^J00FG7UK@;FzkYzCvf;%
zF?HH@O1kHPrwcsMu0uDB*3iLlV2CCKm_w#=nGLrGv<
zJ=-4D^uIm~H10B}b##;%J4mGJiPU}~{X%Wvp-;H~01wsi&;cH;v(+U
z&Ig=o0yWYw!-Vh-5yCnMDcMHV;<$^Dr&ZnW9U%@MJ9u^ek>^X)Td)m>|52E7JCnJe
zafSfDM9wfQ&Ka(FAAQAvI{|6~9d`GxPk&MR563RQUVgjwW^M1m!@Yuej5?Ibri5Yx
zKSe-65+0_>0Y@KyN_hHot5vQ)4gtjl&eNv-gRcekPdTID;9re<8q^O-5QLAY=QFzU
NJJusi9wShG*k4p>CVc<^
diff --git a/utils/__pycache__/mixins.cpython-312.pyc b/utils/__pycache__/mixins.cpython-312.pyc
deleted file mode 100644
index 0c70cbe5548d3b23b4903f91a4e2cca78bb87181..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 2285
zcma)7O>7fa5Pom}?AQ+ZtN95{vH}Wh1qRyE281FNk^rT#LKB1(Qnrit+1X^ho4#E`
zoZ5mz4hWD46;gtFXrx|3>478aiJp70n?u}HD^+bTxw)iLRW9wk^-rCEP@m+Pc{}sw
zy_xxD{C#tC2*KL%M^|#Yh|q6rQ(Hn+SlfZ+9`h6e^h@dg7qcM)~Iwpe75s?T$)6zRF!o+)sGOxSuV>f
zLVWE#uvECg*#o2CR5po~awlYG!p@kwY}u|%W)g(x^3kJmVnU-@!X?y+J>*6`mRTO`
z6-*`Y;V}R5^MhxOkN1JYIY;f9qOKmU=MA{L<8h^%VLIb!7KRg>&U(#PHPg^F
zm#8eJ*`61IjcqUhP#~uR$PM&+c*{-q_OW?-tFP2?rqr@+9X=yZaEw{<@|?(^2jJiw
zN^mpWR3~IX)P3&oSstS-&p;DjV}Y!717T^AN5Ee#A6JNcg}B!UvE(DNX6f>g!!gQ0
z^bnFkTS2H~fi{EK5F2~bD`R5@VRn2ckQ?Zime%?HJJ%Oa-nw4uI8o+NYY%XUk+v|$
zqLBt9$2UYNfPz(j{Tm<)XaeH=3g;nKdMJk`;oIgl$*eamovWjSXHcAE`QEf@jgZ4v
zF`!6*8@gNodu730O*Q#HUOQ9wE>#%pPWv4{Ir$wgvn`=dX8gj
z|G;9EIC7Rn-B1d{l857`56B-3`(yy9P36;P2j!$q)0!JI5E}N2xT|`!p_Gl#mWNJO
z%BYB5FbT85($d5eooUl>J<+tU5lSQMi2@~>u5ffK+q7#YQ|T%Imsk#Tr{l2kBg>WY
zZ>b~KFW0R63T*opZE)C$1G#~I?K)lvcK#S{oxiYf`QGK#s8Wn7%ez;iv1M9}zO@?d
zEk=7+qQ?s-M~l(XQpXUO-XB?ve%bk;^NZJ4qI;Kneu(y#I{IpuNw5^x7Hd}@D^EdK
zKv1*#oT!-~l*6@Zf&L{}$e}4UFtM~(HZ(JU0fU?g
zAkff}*40RNG19&0u0-~fUflH*BRxyvN@Q<2AZ!cW43|UbVDD<%z=IDL2bPY0oB8~W
zLfgPisn|B~Q%n2&ySG1E)QT-VOIr&q`wGE*f3NeXeGofrgggno-zj~^3qX}-FQ}>v
z%&O{zR5fku8I#EoRsA%hnN>~1apBJ;ktXhht$V!VQct*I+oqpSk2fs0l2ir)KeL`-
z8esJ%+rP#Ln@WC)CkNE7#&Zlni*g0P=oWD@G
zc&RY17B7BM7}ow|W|c5l1aXD;wn5iV&5}$i#%c-SW)kpsOC0K_!H;ue?9YQIDhckR
d?|={1QcgRNGLJF-86Emd>cabAXiqJD@3`p-Q2bVzk8WBXWg9=hA4z4K$1xsCZ)S6b%Fj7z!5p5k2
zohsW9OHx5!!)fEWRC8%8j_169m08ISnCISLtIUPP_;u#c*}&s=g_fk}fD(PBpP!-v
zCa9nglPFXrD%FTab%GlaBy*~L&7cNV%St&$BI|k~0{Nq2i1{FH1L299wJjdh0;54Xnw>0Qp7Gc0AJ36?Gyvkc&K?HY{VUv!v+guP5fJn$@=2h0yx3Gk!&)G2eo8RDj7at>TR
z9HIAO3xjg*abGA)Ttop`)b#=3zVtgYpu_Vsmx@>EvUP*HfI;R~iZ}CCWq`?IJz$Ps
zgzo-b$}Fylc;BMd5^tOy!g1!cO<9=#9k4Iae(J5qsfpdx#Dfd%lkNImYHB0d(p$?<
zM^5gaNNt|IKh;f~`8_#$udq?r)b^6&EoFc5-1pNDryoyV*`2)7QTHaVcJjNEGu^jm
zHrbE2ejd+%b*+2bZbhHQ-`GzayO-U_-Y;*>we=r9Yh`~)Tzm3rt(AS0nCQFZHga3E
z?RM^TKIvZgw0pbQ$~{Va{$ynA0O~|(oJ*U+v@Ci5mL+1ARr5&Qk$S?img=_C-%
Date: Thu, 4 Jun 2026 13:02:14 +0200
Subject: [PATCH 06/17] ci: basic ci tools
---
.dockerignore | 5 +
.github/workflows/ci.yml | 32 ++
.gitignore | 3 +
.pre-commit-config.yaml | 54 +++
.python-version | 1 +
Dockerfile | 14 +-
app.py | 18 +-
justfile | 57 +++
pyproject.toml | 84 +++++
requirements.txt | 14 -
tests/conftest.py | 8 +-
tests/test_mixins.py | 2 +-
utils/mixins.py | 22 +-
utils/qr_generator.py | 2 +-
uv.lock | 777 +++++++++++++++++++++++++++++++++++++++
15 files changed, 1042 insertions(+), 51 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 .pre-commit-config.yaml
create mode 100644 .python-version
create mode 100644 justfile
create mode 100644 pyproject.toml
delete mode 100644 requirements.txt
create mode 100644 uv.lock
diff --git a/.dockerignore b/.dockerignore
index 9791a84..8d9b7c8 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -4,3 +4,8 @@ __pycache__/
*.pyc
.idea/
*.log
+.git/
+.ruff_cache/
+.ty_cache/
+.pytest_cache/
+tests/result/
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9f6f944
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,32 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: astral-sh/setup-uv@v6
+ with:
+ python-version: "3.14"
+ - run: uv sync
+ - run: uv run ruff check .
+ - run: uv run ruff format --check .
+ - run: uv run ty check
+ - run: uv run codespell
+ - run: uv run bandit -r . -c pyproject.toml -q
+
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: astral-sh/setup-uv@v6
+ with:
+ python-version: "3.14"
+ - run: uv sync
+ - run: uv run pytest -m "not slow"
diff --git a/.gitignore b/.gitignore
index 675e6b4..bdc846b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,6 @@ __pycache__/
*.pyc
.venv/
.pytest_cache/
+.ruff_cache/
+.ty_cache/
+tests/result/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..06e2b26
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,54 @@
+default_install_hook_types: [pre-commit, post-merge]
+exclude: 'static/'
+
+repos:
+- repo: local
+ hooks:
+ - id: uv-lock-check
+ name: uv lock --check
+ entry: uv lock --check
+ language: system
+ pass_filenames: false
+ files: ^(pyproject\.toml|uv\.lock)$
+
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+
+- repo: local
+ hooks:
+ - id: ruff-check
+ name: ruff check
+ entry: uv run ruff check
+ language: system
+ types_or: [python, pyi]
+ pass_filenames: false
+ always_run: true
+ - id: ruff-format
+ name: ruff format
+ entry: uv run ruff format --check
+ language: system
+ types_or: [python, pyi]
+ pass_filenames: false
+ always_run: true
+ - id: codespell
+ name: codespell
+ entry: uv run codespell
+ language: system
+ types_or: [python, markdown, yaml]
+ pass_filenames: false
+ always_run: true
+ - id: ty-check
+ name: ty check
+ entry: uv run ty check
+ language: system
+ pass_filenames: false
+ always_run: true
+ - id: bandit
+ name: bandit
+ entry: uv run bandit -r . -c pyproject.toml -q
+ language: system
+ pass_filenames: false
+ always_run: true
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..a6d9ada
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.14.5
diff --git a/Dockerfile b/Dockerfile
index c148367..f7d1d06 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,8 @@
-FROM python:3.12
+FROM python:3.14-slim
LABEL authors="AM"
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
WORKDIR /app
-COPY requirements.txt ./
-
-RUN python -m pip install --upgrade pip && \
- pip install -r requirements.txt
-
+COPY pyproject.toml uv.lock ./
+RUN uv sync --frozen --no-dev
COPY . .
-
-CMD ["python", "app.py"]
-
+CMD ["uv", "run", "--no-sync", "python", "app.py"]
diff --git a/app.py b/app.py
index 8a3f1de..f69ec34 100644
--- a/app.py
+++ b/app.py
@@ -2,14 +2,14 @@
import os
import secrets
-from flask import Flask, abort, render_template, request, send_file, jsonify
+from flask import Flask, abort, jsonify, render_template, request, send_file
from flask_wtf import CSRFProtect
from flask_wtf.csrf import CSRFError
+
from utils.logger import AppLogger
from utils.qr_generator import QRCodeGenerator
from utils.result_store import ResultStore
-
AppLogger.configure_logger()
app = Flask(__name__)
@@ -39,7 +39,7 @@ def generate():
text = request.form.get('text', '')
try:
size = int(request.form.get('size', ''))
- except (TypeError, ValueError):
+ except TypeError, ValueError:
return jsonify({'success': False, 'errors': ['Size must be a valid integer.']}), 400
raw_image = request.files.get('image')
image = raw_image if (raw_image and raw_image.filename) else None
@@ -51,11 +51,13 @@ def generate():
if not success:
return jsonify({'success': False, 'errors': errors})
- token = store.put({
- 'qr': generator.qr_png,
- 'artistic': generator.artistic_png,
- 'artistic_ext': generator.artistic_ext,
- })
+ token = store.put(
+ {
+ 'qr': generator.qr_png,
+ 'artistic': generator.artistic_png,
+ 'artistic_ext': generator.artistic_ext,
+ }
+ )
return jsonify({'success': True, 'id': token})
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..0ebe655
--- /dev/null
+++ b/justfile
@@ -0,0 +1,57 @@
+set shell := ["pwsh", "-NoLogo", "-Command"]
+
+# List all available recipes
+help:
+ @just --list
+
+# Install all dependencies including dev group
+install:
+ uv sync
+
+# Run all linters (ruff check, ty, codespell, bandit)
+lint:
+ uv run ruff check .
+ uv run ty check
+ uv run codespell
+ uv run bandit -r . -c pyproject.toml -q
+
+# Format code and auto-fix lint issues
+format:
+ uv run ruff format .
+ uv run ruff check --fix .
+
+# Run all tests excluding slow
+test:
+ uv run pytest -m "not slow"
+
+# Run only unit tests
+test-unit:
+ uv run pytest -m unit -v
+
+# Run only integration tests
+test-integration:
+ uv run pytest -m integration -v
+
+# Start Flask development server
+runserver:
+ uv run python app.py
+
+# Start Docker services (build)
+up:
+ docker-compose up -d --build
+
+# Stop Docker services
+down:
+ docker-compose down
+
+# Run pre-commit hooks on all files
+precommit:
+ uv run pre-commit run --all-files
+
+# Commit with pre-commit checks and commitizen
+commit:
+ uv run pre-commit run && uv run cz commit
+
+# Bump version using commitizen
+bump:
+ uv run cz bump
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..b9cbcf5
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,84 @@
+[project]
+name = "qr-generator"
+version = "0.1.0"
+description = "Flask QR code generator with artistic image overlay"
+requires-python = ">=3.14"
+dependencies = [
+ "Flask>=3.1.3",
+ "Flask-WTF>=1.2.1",
+ "pillow>=11.0",
+ "qrcode-artistic>=3.0.2",
+ "segno>=1.6.1",
+]
+
+[dependency-groups]
+dev = [
+ "pre-commit>=4.2.0",
+ "ruff>=0.11.6",
+ "ty>=0.0.20",
+ "codespell>=2.4.1",
+ "bandit[toml]",
+ "pytest>=9.0.2",
+ "pytest-flask",
+ "pytest-cov>=6.1.1",
+ "commitizen>=4.13.9",
+]
+
+[tool.uv]
+package = false
+
+[tool.ruff]
+line-length = 124
+
+[tool.ruff.lint]
+select = ["E", "F", "UP", "B", "SIM", "I", "RUF"]
+ignore = ["RUF012"]
+
+[tool.ruff.lint.isort]
+known-first-party = ["utils", "app"]
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**" = ["F401", "F841"]
+"test_*.py" = ["F401", "F841"]
+
+[tool.ruff.format]
+quote-style = "single"
+indent-style = "space"
+docstring-code-format = true
+docstring-code-line-length = 124
+
+[tool.pytest.ini_options]
+pythonpath = ["."]
+python_files = ["test_*.py"]
+addopts = "--strict-markers"
+markers = [
+ "unit: fast isolated tests with no IO",
+ "integration: tests that touch routes or the store",
+ "slow: tests that take more than a few seconds",
+]
+
+[tool.ty.environment]
+python-version = "3.14"
+extra-paths = ["."]
+
+[tool.codespell]
+skip = "uv.lock,./static,./.venv"
+builtin = "clear"
+quiet-level = 3
+
+[tool.bandit]
+exclude_dirs = [".venv", "tests"]
+skips = ["B104"]
+
+[tool.coverage.run]
+source = ["utils", "app"]
+
+[[tool.ty.overrides]]
+include = ["utils/qr_generator.py"]
+rules = { call-non-callable = "ignore" }
+
+[tool.commitizen]
+name = "cz_conventional_commits"
+version = "0.1.0"
+tag_format = "v$version"
+version_files = ["pyproject.toml:version"]
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 9080073..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-blinker==1.7.0
-click==8.1.7
-colorama==0.4.6
-Flask==3.0.3
-itsdangerous==2.1.2
-Jinja2==3.1.3
-MarkupSafe==2.1.5
-pillow==10.3.0
-qrcode-artistic==3.0.2
-segno==1.6.1
-Werkzeug==3.0.2
-Flask-WTF==1.2.1
-pytest
-pytest-flask
diff --git a/tests/conftest.py b/tests/conftest.py
index c345c49..add6bf0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,7 +3,8 @@
import pytest
-from app import app as flask_app, store
+from app import app as flask_app
+from app import store
@pytest.fixture
@@ -20,10 +21,7 @@ def client(app):
@pytest.fixture
def png_bytes():
- return base64.b64decode(
- 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12NgAAIABQ'
- 'AABjkB6QAAAABJRU5ErkJggg=='
- )
+ return base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12NgAAIABQAABjkB6QAAAABJRU5ErkJggg==')
@pytest.fixture
diff --git a/tests/test_mixins.py b/tests/test_mixins.py
index aa4807a..598356d 100644
--- a/tests/test_mixins.py
+++ b/tests/test_mixins.py
@@ -1,7 +1,7 @@
import io
from unittest.mock import MagicMock
-from utils.mixins import NotificationMixin, MIN_SIZE, MAX_SIZE
+from utils.mixins import MAX_SIZE, MIN_SIZE, NotificationMixin
class Validator(NotificationMixin):
diff --git a/utils/mixins.py b/utils/mixins.py
index da62f6f..840fd19 100644
--- a/utils/mixins.py
+++ b/utils/mixins.py
@@ -8,32 +8,28 @@ class NotificationMixin:
@staticmethod
def validate_text(text: str) -> tuple[bool, str]:
if len(text) > 55:
- return False, "Text length should not exceed 55 characters."
- return True, ""
+ return False, 'Text length should not exceed 55 characters.'
+ return True, ''
@staticmethod
def validate_size(size: int) -> tuple[bool, str]:
if not MIN_SIZE <= size <= MAX_SIZE:
- return False, f"Size should be between {MIN_SIZE} and {MAX_SIZE}."
- return True, ""
+ return False, f'Size should be between {MIN_SIZE} and {MAX_SIZE}.'
+ return True, ''
@staticmethod
def validate_image(image: FileStorage | None) -> tuple[bool, str]:
allowed_extensions = {'png', 'jpg', 'jpeg', 'gif'}
- if image and image.filename.split('.')[-1].lower() not in allowed_extensions:
- return False, "Image should be in PNG, JPG, JPEG, or GIF format."
+ if image and image.filename and image.filename.split('.')[-1].lower() not in allowed_extensions:
+ return False, 'Image should be in PNG, JPG, JPEG, or GIF format.'
if image:
size_bytes = len(image.read())
image.seek(0)
if size_bytes > 5 * 1024 * 1024: # 5 MB
- return False, "Image size should not exceed 5 MB."
- return True, ""
+ return False, 'Image size should not exceed 5 MB.'
+ return True, ''
def validate_data(self, text: str, size: int, image: FileStorage | None) -> tuple[bool, list[str]]:
- validations = [
- self.validate_text(text),
- self.validate_size(size),
- self.validate_image(image)
- ]
+ validations = [self.validate_text(text), self.validate_size(size), self.validate_image(image)]
errors = [error for valid, error in validations if not valid]
return len(errors) == 0, errors
diff --git a/utils/qr_generator.py b/utils/qr_generator.py
index 3c282fa..fc6746d 100644
--- a/utils/qr_generator.py
+++ b/utils/qr_generator.py
@@ -1,9 +1,9 @@
import io
import segno
+from werkzeug.datastructures import FileStorage
from utils.mixins import NotificationMixin
-from werkzeug.datastructures import FileStorage
class QRCodeGenerator(NotificationMixin):
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..a19812b
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,777 @@
+version = 1
+revision = 3
+requires-python = ">=3.14"
+
+[[package]]
+name = "argcomplete"
+version = "3.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" },
+]
+
+[[package]]
+name = "bandit"
+version = "1.9.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "pyyaml" },
+ { name = "rich" },
+ { name = "stevedore" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" },
+]
+
+[[package]]
+name = "blinker"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
+ { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
+ { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
+ { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
+ { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
+ { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
+ { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
+ { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
+ { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
+ { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
+]
+
+[[package]]
+name = "codespell"
+version = "2.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "commitizen"
+version = "4.16.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "argcomplete" },
+ { name = "charset-normalizer" },
+ { name = "colorama" },
+ { name = "decli" },
+ { name = "deprecated" },
+ { name = "jinja2" },
+ { name = "packaging" },
+ { name = "prompt-toolkit" },
+ { name = "pyyaml" },
+ { name = "questionary" },
+ { name = "termcolor" },
+ { name = "tomlkit" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/cc/d87b094ef858c67febcd1d8902352c84b42c9ebc8221d6f2e9d553273358/commitizen-4.16.3.tar.gz", hash = "sha256:5cdca4c02715cc770312f4b505c65a6c39024c73ece41b943bccaf81c44436ed", size = 66772, upload-time = "2026-05-30T06:34:21.247Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/35/c7995b1e66159193dd31ed5628d59acbaf4611811645eedf0fb2d5a91946/commitizen-4.16.3-py3-none-any.whl", hash = "sha256:ce1be39fe98a16725fd0c960daf0f360acac86db7ae8db1e1df8d3541005b5be", size = 88927, upload-time = "2026-05-30T06:34:20.006Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.14.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" },
+ { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" },
+ { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" },
+ { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" },
+ { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" },
+ { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" },
+ { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" },
+ { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
+]
+
+[[package]]
+name = "decli"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656", size = 7564, upload-time = "2025-06-01T15:23:41.25Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" },
+]
+
+[[package]]
+name = "deprecated"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/86/b2/d6fc3f2347f43dada79e5ff118493e8109c98400a0e29a1d5264a3aa479b/distlib-0.4.1.tar.gz", hash = "sha256:c3804d0d2d4b5fcd44036eb860cb6660485fcdf5c2aba53dc324d805837ea65b", size = 610526, upload-time = "2026-06-02T11:17:40.691Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/18/3497c4fa83a76dcb154923fd2075522e8dd6995ecee4093c00ae18160046/distlib-0.4.1-py2.py3-none-any.whl", hash = "sha256:9c2c552c68cbadc619f2d0ed3a69e27c351a3f4c9baa9ffb7df9e9cdc3d19a97", size = 469216, upload-time = "2026-06-02T11:17:38.779Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.29.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" },
+]
+
+[[package]]
+name = "flask"
+version = "3.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "blinker" },
+ { name = "click" },
+ { name = "itsdangerous" },
+ { name = "jinja2" },
+ { name = "markupsafe" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
+]
+
+[[package]]
+name = "flask-wtf"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "itsdangerous" },
+ { name = "wtforms" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/f1/605a56d4ea217b307f3e6f4d663e0351253d85d841edc93ba559f0648e19/flask_wtf-1.3.0.tar.gz", hash = "sha256:61d5dabc50c3df885c297dcbd80810443a5d632106c8a69cab8ce740f0cdd7cc", size = 50414, upload-time = "2026-04-23T07:41:55.096Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/d2/97adf2ec7af95522573e6dd5493ee84792d0fbfb2def010c4a581b8d6e5e/flask_wtf-1.3.0-py3-none-any.whl", hash = "sha256:dc5e3a4ce97f75c47bf6c1c72ad2c3b7bdf579a2ed13aebcc5d3d81fe2571160", size = 13959, upload-time = "2026-04-23T07:41:53.828Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.19"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "itsdangerous"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
+ { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
+ { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
+ { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
+ { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
+ { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
+]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.51"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage" },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
+]
+
+[[package]]
+name = "pytest-flask"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "pytest" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/23/32b36d2f769805c0f3069ca8d9eeee77b27fcf86d41d40c6061ddce51c7d/pytest-flask-1.3.0.tar.gz", hash = "sha256:58be1c97b21ba3c4d47e0a7691eb41007748506c36bf51004f78df10691fa95e", size = 35816, upload-time = "2023-10-23T14:53:20.696Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/03/7a917fda3d0e96b4e80ab1f83a6628ec4ee4a882523b49417d3891bacc9e/pytest_flask-1.3.0-py3-none-any.whl", hash = "sha256:c0e36e6b0fddc3b91c4362661db83fa694d1feb91fa505475be6732b5bc8c253", size = 13105, upload-time = "2023-10-23T14:53:18.959Z" },
+]
+
+[[package]]
+name = "python-discovery"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "qr-generator"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "flask" },
+ { name = "flask-wtf" },
+ { name = "pillow" },
+ { name = "qrcode-artistic" },
+ { name = "segno" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "bandit" },
+ { name = "codespell" },
+ { name = "commitizen" },
+ { name = "pre-commit" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "pytest-flask" },
+ { name = "ruff" },
+ { name = "ty" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "flask", specifier = ">=3.1.3" },
+ { name = "flask-wtf", specifier = ">=1.2.1" },
+ { name = "pillow", specifier = ">=11.0" },
+ { name = "qrcode-artistic", specifier = ">=3.0.2" },
+ { name = "segno", specifier = ">=1.6.1" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "bandit", extras = ["toml"] },
+ { name = "codespell", specifier = ">=2.4.1" },
+ { name = "commitizen", specifier = ">=4.13.9" },
+ { name = "pre-commit", specifier = ">=4.2.0" },
+ { name = "pytest", specifier = ">=9.0.2" },
+ { name = "pytest-cov", specifier = ">=6.1.1" },
+ { name = "pytest-flask" },
+ { name = "ruff", specifier = ">=0.11.6" },
+ { name = "ty", specifier = ">=0.0.20" },
+]
+
+[[package]]
+name = "qrcode-artistic"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pillow" },
+ { name = "segno" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4f/cf/5fff014b4ba48c7e985343bc59827e134a11bae71227c15a47bee3b999aa/qrcode-artistic-3.0.2.tar.gz", hash = "sha256:eb71f12673c89f638cf7252554a74ecbe4a22f1ce15920caa53da75a48c181f4", size = 9336, upload-time = "2023-11-27T18:31:24.824Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/62/81f17a375319b3d0242a3b93dd2d30ce16e5592544e1762f8c2e7514c9d2/qrcode_artistic-3.0.2-py3-none-any.whl", hash = "sha256:a2aa751a7f0220767f70842cceec206b21f9207478b9612d468273a1f46429ae", size = 7636, upload-time = "2023-11-27T18:31:22.776Z" },
+]
+
+[[package]]
+name = "questionary"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "prompt-toolkit" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
+]
+
+[[package]]
+name = "rich"
+version = "15.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.15.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
+ { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
+ { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
+ { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
+ { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
+ { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
+]
+
+[[package]]
+name = "segno"
+version = "1.6.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/2e/b396f750c53f570055bf5a9fc1ace09bed2dff013c73b7afec5702a581ba/segno-1.6.6.tar.gz", hash = "sha256:e60933afc4b52137d323a4434c8340e0ce1e58cec71439e46680d4db188f11b3", size = 1628586, upload-time = "2025-03-12T22:12:53.324Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d6/02/12c73fd423eb9577b97fc1924966b929eff7074ae6b2e15dd3d30cb9e4ae/segno-1.6.6-py3-none-any.whl", hash = "sha256:28c7d081ed0cf935e0411293a465efd4d500704072cdb039778a2ab8736190c7", size = 76503, upload-time = "2025-03-12T22:12:48.106Z" },
+]
+
+[[package]]
+name = "stevedore"
+version = "5.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e9/88/35e4d27d9177d7df76d060e0a18f69c6c5794c96960c94042e20a12c8ba2/stevedore-5.8.0.tar.gz", hash = "sha256:b49867b32ca3016e94100e68dbf26e72aa7b8708d0a3f73c08aeb220370ac715", size = 514710, upload-time = "2026-05-18T09:15:27.731Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/ac/19f9941c74add59d17694930ec8105d5eddeee4ce56dd8632b765ca16d6c/stevedore-5.8.0-py3-none-any.whl", hash = "sha256:88eede9e66ca80e34085b9174e2327da2c61ac91f24f70e41c3ad76e4bb4872b", size = 54553, upload-time = "2026-05-18T09:15:25.82Z" },
+]
+
+[[package]]
+name = "termcolor"
+version = "3.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" },
+]
+
+[[package]]
+name = "tomlkit"
+version = "0.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/51/db/03eaf4331631ef6b27d6e3c9b68c54dc6f0d63d87201fed600cc409307fd/tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3", size = 161875, upload-time = "2026-05-10T07:38:22.245Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738", size = 41328, upload-time = "2026-05-10T07:38:23.517Z" },
+]
+
+[[package]]
+name = "ty"
+version = "0.0.43"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0d/37/4ec04de0659b93be37d956dfceca13b1ecab9c959f28d8a1d5e514603f36/ty-0.0.43.tar.gz", hash = "sha256:ea4cff50548f2a1877e848d3abe9e293cde8ab94757a7eb93fc0d4013f98be8e", size = 5798429, upload-time = "2026-06-04T00:52:10.013Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/74/1916026a78f20019a2f03adbd6fb4430ddb7ce1e52c2e17a90856a6d192e/ty-0.0.43-py3-none-linux_armv6l.whl", hash = "sha256:3bf70f5446480562bf6c9f639df4b5cb60716b8f8d1a6b8e5811d5c7eccd8bf2", size = 11598153, upload-time = "2026-06-04T00:52:20.646Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/af/58bb0089d2635216c8fa6612dd486a3f986d0ab1c46a41527ab95e57f0e3/ty-0.0.43-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7184741f8b15425a1bc64b950ad005cb353573288ac0e8a04f5481ceb3832596", size = 11357811, upload-time = "2026-06-04T00:52:24.683Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/9c/32c6b14f3feddf87b59c7a50709e2b3da408258f2f583f05575f77bc8f7b/ty-0.0.43-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c306379ca9a35f6ae5270fe9bda7af4b46d91822725a2586d78c8b9b5493b62", size = 10772024, upload-time = "2026-06-04T00:52:14.312Z" },
+ { url = "https://files.pythonhosted.org/packages/09/fa/98aa4a74bd00cd5efc424923cd1daffbf1e40a0338041cafb203379d746f/ty-0.0.43-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d624b884c9c1fd244ad2a5f026364e7162a22b3f537025941ada2e363e676414", size = 11291034, upload-time = "2026-06-04T00:52:37.249Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/db/4de086c38ce96dcada2bd451f43171d2c237f96d8ed19a1ea8fe51bb8ef4/ty-0.0.43-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:281fc4c00fbc196045141faa085055bddc58846b04a2800204701415a1b9c6aa", size = 11364724, upload-time = "2026-06-04T00:52:33.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d3/e3cd8e3233a6fd8362a49aa025b79e9f40151a2a86d811ace154c6eb7445/ty-0.0.43-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57d6cc28de89024b48d1788e4758c05299d5749d4a51c02e71ac655ec23d9a5", size = 11890555, upload-time = "2026-06-04T00:52:22.711Z" },
+ { url = "https://files.pythonhosted.org/packages/80/7b/6f46d444e8241606bbde098df3dca93f2ec0b834a42055db85ee7d33646f/ty-0.0.43-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a1d6ad6c5e7792c7eac0a01e550f2c2004462e01a64a91ea1636aba6fef6e71", size = 12450968, upload-time = "2026-06-04T00:52:28.94Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/e1/79fbe51f2e4b9d8347f2013cd7ed0b63f3b499038c02dc0357e9b28a3a47/ty-0.0.43-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66d474395d7635fb618bdbb58b4e3360259a2056d0a5621b82754b9da2cd8a04", size = 12064187, upload-time = "2026-06-04T00:52:12.039Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/3f/c758a3a8df5b90d331f2b60c8f16021ee64d75e78f99d67cc4efc9bf5f4b/ty-0.0.43-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2663a0003a8b60fb98db7f6f6e673df80b21d0fe3a9868a26fb06b4e049b6fc4", size = 11943208, upload-time = "2026-06-04T00:52:31.14Z" },
+ { url = "https://files.pythonhosted.org/packages/54/5f/f516442749cf1b45ca6720a5d41df2738a486ed9ace774c03d515db89084/ty-0.0.43-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d5a6c352d374d889189d5ec82b54b26a5885f769f7b7787f7f875500dcb8673e", size = 12143572, upload-time = "2026-06-04T00:52:18.457Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/bf/0d83c7f43bf4c10f3678bfe7d938e51c445298c7b923f155c5204730c2df/ty-0.0.43-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e7dbbeedfad3ca250d74fcc355fa9ab6b38d2a17f22d6304f615716939dbbb27", size = 11279355, upload-time = "2026-06-04T00:52:26.726Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/de/a6c978bef6d9e949f79f4782d9e4ee4df0893713e73b055d84c1a5116b9a/ty-0.0.43-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:24b18a0273ee46154996cfcfa27438f851f440c925587ec200df6f98dffe67d3", size = 11408412, upload-time = "2026-06-04T00:52:35.282Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/b1/d13857c23867f0f76b92e38e5841c64ca5e76dc5d4bf27f52cb81d8ab685/ty-0.0.43-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ef681951520d692b7e9c0b5e56aacf4f98ccae47cf6ffccaf2c7b6b33dc226e", size = 11541709, upload-time = "2026-06-04T00:52:16.451Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f1/cd6afc6f6a687e238bf5e12189f7920e81a0bdef6c3dba4c784ef140f7d9/ty-0.0.43-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2af105de7437143aa4676b28016b5bee661aaaa4eff52be5867fb25119641ceb", size = 12041266, upload-time = "2026-06-04T00:52:43.541Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/ba/51ca7c3335da2b8d0a3e477fa4986be9f4a53b05bfab862967d8d2e6ca60/ty-0.0.43-py3-none-win32.whl", hash = "sha256:e4773115b0d6486ee30f1657fc8bdffe7e3a3f5300ab77ef2495da6e83e4694f", size = 10858724, upload-time = "2026-06-04T00:52:07.843Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/29/5d80453e5f7c520145fa058851da87230dbd7ca761a7675447a9fe504e0b/ty-0.0.43-py3-none-win_amd64.whl", hash = "sha256:48d3545094a4ae6395492c7e6ac90550fce969e0ed2815fbf8c5da9756676b7d", size = 11976157, upload-time = "2026-06-04T00:52:41.438Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/ed/befe5a543e5b95e754ed38ee95239e44efda9bc5f578db4ac1bc8dd758d6/ty-0.0.43-py3-none-win_arm64.whl", hash = "sha256:740ca33d7f75f655a4e7d475bc42dfb825c13219bb073fad30fcc04d35790c74", size = 11308680, upload-time = "2026-06-04T00:52:39.233Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "21.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+ { name = "python-discovery" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e1/0d/4e93c8e6d1001a75763f87d8f5ecda8ebc7f4aa2153dddfaf4ae8892821a/virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", size = 7613326, upload-time = "2026-05-31T17:01:22.827Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" },
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
+]
+
+[[package]]
+name = "wrapt"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab", size = 80879, upload-time = "2026-05-22T14:48:51.802Z" },
+ { url = "https://files.pythonhosted.org/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c", size = 81462, upload-time = "2026-05-22T14:48:53.18Z" },
+ { url = "https://files.pythonhosted.org/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c", size = 167251, upload-time = "2026-05-22T14:48:54.562Z" },
+ { url = "https://files.pythonhosted.org/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e", size = 166316, upload-time = "2026-05-22T14:48:56.065Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f", size = 157952, upload-time = "2026-05-22T14:48:57.924Z" },
+ { url = "https://files.pythonhosted.org/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508", size = 166130, upload-time = "2026-05-22T14:48:59.384Z" },
+ { url = "https://files.pythonhosted.org/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5", size = 156604, upload-time = "2026-05-22T14:49:00.921Z" },
+ { url = "https://files.pythonhosted.org/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283", size = 166007, upload-time = "2026-05-22T14:49:02.332Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243", size = 78327, upload-time = "2026-05-22T14:49:03.722Z" },
+ { url = "https://files.pythonhosted.org/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b", size = 81144, upload-time = "2026-05-22T14:49:05.093Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36", size = 79569, upload-time = "2026-05-22T14:49:06.413Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188", size = 82892, upload-time = "2026-05-22T14:49:07.925Z" },
+ { url = "https://files.pythonhosted.org/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199", size = 83333, upload-time = "2026-05-22T14:49:09.257Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413", size = 202899, upload-time = "2026-05-22T14:49:10.572Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956", size = 209986, upload-time = "2026-05-22T14:49:12.229Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e", size = 194893, upload-time = "2026-05-22T14:49:14.139Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85", size = 205636, upload-time = "2026-05-22T14:49:15.714Z" },
+ { url = "https://files.pythonhosted.org/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181", size = 192267, upload-time = "2026-05-22T14:49:17.283Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a", size = 198378, upload-time = "2026-05-22T14:49:18.785Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85", size = 80226, upload-time = "2026-05-22T14:49:20.264Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50", size = 83835, upload-time = "2026-05-22T14:49:21.634Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00", size = 80722, upload-time = "2026-05-22T14:49:23.59Z" },
+ { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" },
+]
+
+[[package]]
+name = "wtforms"
+version = "3.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/91/ed9b517da898e3fb747566aa3c12a734bd64ea7449a0d25ec74ce8f8b8eb/wtforms-3.2.2.tar.gz", hash = "sha256:7b00c73f8670f35d4edb0293dcd81b980528bee72fd662b182aaba27ae570b93", size = 139583, upload-time = "2026-05-03T05:53:44.147Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/76/bb225c8300f3a0ba28e01df51419c6c9574a297c43d71b29048e03b65deb/wtforms-3.2.2-py3-none-any.whl", hash = "sha256:72b90d5d921bd3119252069cf0301e9c13915f9e52792652bc91c5dda4b79e56", size = 158656, upload-time = "2026-05-03T05:53:46.072Z" },
+]
From ecc6afe31ae81ba4c4b75a30a48c0fad7f0627e1 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 16:17:38 +0200
Subject: [PATCH 07/17] ci: traefic compose
---
docker-compose-traefik.yml | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 docker-compose-traefik.yml
diff --git a/docker-compose-traefik.yml b/docker-compose-traefik.yml
new file mode 100644
index 0000000..1fc8f07
--- /dev/null
+++ b/docker-compose-traefik.yml
@@ -0,0 +1,16 @@
+services:
+ web:
+ build: .
+ container_name: qr_web
+ networks:
+ - proxy
+ labels:
+ - traefik.enable=true
+ - traefik.http.routers.qr.rule=Host(`qr.home`)
+ - traefik.http.services.qr.loadbalancer.server.port=5000
+ volumes:
+ - .:/app
+
+networks:
+ proxy:
+ external: true
From f2872f85e0b22eeacb755b7cb1878e87e95db311 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 16:23:02 +0200
Subject: [PATCH 08/17] ci(justfile): deeplay commands
---
justfile | 39 +++++++++++++++++++++++++++++++--------
1 file changed, 31 insertions(+), 8 deletions(-)
diff --git a/justfile b/justfile
index 0ebe655..408150f 100644
--- a/justfile
+++ b/justfile
@@ -1,5 +1,8 @@
set shell := ["pwsh", "-NoLogo", "-Command"]
+server := "tartuffe@192.168.0.150"
+server_dir := "~/qr_generator"
+
# List all available recipes
help:
@just --list
@@ -20,6 +23,10 @@ format:
uv run ruff format .
uv run ruff check --fix .
+# Start Flask development server
+runserver:
+ uv run python app.py
+
# Run all tests excluding slow
test:
uv run pytest -m "not slow"
@@ -32,20 +39,36 @@ test-unit:
test-integration:
uv run pytest -m integration -v
-# Start Flask development server
-runserver:
- uv run python app.py
-
# Start Docker services (build)
-up:
+docker-up:
docker-compose up -d --build
# Stop Docker services
-down:
+docker-down:
docker-compose down
+# Start Docker services with Traefik (local)
+docker-up-traefik:
+ docker-compose -f docker-compose-traefik.yml up -d --build
+
+# Stop Docker services with Traefik (local)
+docker-down-traefik:
+ docker-compose -f docker-compose-traefik.yml down
+
+# Deploy to home server: git pull + docker compose up
+deploy:
+ ssh {{server}} "cd {{server_dir}} && git pull && docker compose -f docker-compose-traefik.yml up -d --build"
+
+# Stop deployment on home server
+deploy-down:
+ ssh {{server}} "cd {{server_dir}} && docker compose -f docker-compose-traefik.yml down"
+
+# Stream live logs from home server
+deploy-logs:
+ ssh {{server}} "docker logs -f qr_web"
+
# Run pre-commit hooks on all files
-precommit:
+git-precommit:
uv run pre-commit run --all-files
# Commit with pre-commit checks and commitizen
@@ -53,5 +76,5 @@ commit:
uv run pre-commit run && uv run cz commit
# Bump version using commitizen
-bump:
+git-bump:
uv run cz bump
From 5cdfaa4cb2f51cfd3b4ff8fd640a6f5b99ee0845 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 16:45:12 +0200
Subject: [PATCH 09/17] ci: separation dev vs deeploy
---
Makefile | 15 +++++++++++++++
docker-compose-traefik.yml | 3 ---
justfile | 22 +++++++++++-----------
3 files changed, 26 insertions(+), 14 deletions(-)
create mode 100644 Makefile
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0a6aa80
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,15 @@
+.PHONY: up down restart logs build
+
+up:
+ docker compose -f docker-compose-traefik.yml up -d --build
+
+down:
+ docker compose -f docker-compose-traefik.yml down
+
+restart: down up
+
+logs:
+ docker logs -f qr_web
+
+build:
+ docker compose -f docker-compose-traefik.yml build
diff --git a/docker-compose-traefik.yml b/docker-compose-traefik.yml
index 1fc8f07..7d4e463 100644
--- a/docker-compose-traefik.yml
+++ b/docker-compose-traefik.yml
@@ -8,9 +8,6 @@ services:
- traefik.enable=true
- traefik.http.routers.qr.rule=Host(`qr.home`)
- traefik.http.services.qr.loadbalancer.server.port=5000
- volumes:
- - .:/app
-
networks:
proxy:
external: true
diff --git a/justfile b/justfile
index 408150f..c6bb27b 100644
--- a/justfile
+++ b/justfile
@@ -39,29 +39,29 @@ test-unit:
test-integration:
uv run pytest -m integration -v
-# Start Docker services (build)
+# Start Docker services â dev (plain docker-compose)
docker-up:
- docker-compose up -d --build
+ docker compose up -d --build
-# Stop Docker services
+# Stop Docker services â dev
docker-down:
- docker-compose down
+ docker compose down
-# Start Docker services with Traefik (local)
+# Start Docker services with Traefik â local test
docker-up-traefik:
- docker-compose -f docker-compose-traefik.yml up -d --build
+ docker compose -f docker-compose-traefik.yml up -d --build
-# Stop Docker services with Traefik (local)
+# Stop Docker services with Traefik â local test
docker-down-traefik:
- docker-compose -f docker-compose-traefik.yml down
+ docker compose -f docker-compose-traefik.yml down
-# Deploy to home server: git pull + docker compose up
+# Deploy to home server: git pull + make up
deploy:
- ssh {{server}} "cd {{server_dir}} && git pull && docker compose -f docker-compose-traefik.yml up -d --build"
+ ssh {{server}} "cd {{server_dir}} && git pull && make up"
# Stop deployment on home server
deploy-down:
- ssh {{server}} "cd {{server_dir}} && docker compose -f docker-compose-traefik.yml down"
+ ssh {{server}} "cd {{server_dir}} && make down"
# Stream live logs from home server
deploy-logs:
From 699fdfc5a863f59d791956ed50133d5e95ca9138 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 16:50:03 +0200
Subject: [PATCH 10/17] ci(traefik): conf
---
docker-compose-traefik.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/docker-compose-traefik.yml b/docker-compose-traefik.yml
index 7d4e463..b624059 100644
--- a/docker-compose-traefik.yml
+++ b/docker-compose-traefik.yml
@@ -6,7 +6,9 @@ services:
- proxy
labels:
- traefik.enable=true
- - traefik.http.routers.qr.rule=Host(`qr.home`)
+ - traefik.docker.network=proxy
+ - traefik.http.routers.qr.rule=Host(`qrcode.home`)
+ - traefik.http.routers.qr.entrypoints=web
- traefik.http.services.qr.loadbalancer.server.port=5000
networks:
proxy:
From c24603a42083d4ca2e57200d29217197b3d1c0b3 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 17:09:50 +0200
Subject: [PATCH 11/17] fix(frontend): bootstrap classes
---
templates/index.html | 32 +++++++++++++++++++++-----------
templates/result.html | 13 ++++++-------
2 files changed, 27 insertions(+), 18 deletions(-)
diff --git a/templates/index.html b/templates/index.html
index 9ace205..a92be6e 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -15,22 +15,32 @@
-
QR Code Generator
+
QR Code Generator
diff --git a/templates/result.html b/templates/result.html
index 342f563..c7e94f4 100644
--- a/templates/result.html
+++ b/templates/result.html
@@ -14,17 +14,16 @@
From b3e6873cfee4b1408f74f5bca6d28909e3424dbf Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 17:17:17 +0200
Subject: [PATCH 12/17] feat(engine): sfitch for microqr
---
app.py | 3 ++-
templates/index.html | 5 +++++
utils/qr_generator.py | 6 +++++-
3 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/app.py b/app.py
index f69ec34..e720001 100644
--- a/app.py
+++ b/app.py
@@ -44,9 +44,10 @@ def generate():
raw_image = request.files.get('image')
image = raw_image if (raw_image and raw_image.filename) else None
color = request.form.get('color') or None
+ micro = request.form.get('micro') == 'on'
generator = QRCodeGenerator()
- success, errors = generator.generate_qr(text, size, image, color)
+ success, errors = generator.generate_qr(text, size, image, color, micro)
if not success:
return jsonify({'success': False, 'errors': errors})
diff --git a/templates/index.html b/templates/index.html
index a92be6e..c76b0da 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -40,6 +40,11 @@
QR Code Generator
+
+
+
+
+
diff --git a/utils/qr_generator.py b/utils/qr_generator.py
index fc6746d..d1cebec 100644
--- a/utils/qr_generator.py
+++ b/utils/qr_generator.py
@@ -18,12 +18,16 @@ def generate_qr(
size: int,
image: FileStorage | None,
color: str | None = None,
+ micro: bool = False,
) -> tuple[bool, list[str]]:
valid, errors = self.validate_data(text, size, image)
if not valid:
return False, errors
- qr = segno.make(text)
+ try:
+ qr = segno.make(text, micro=True) if micro else segno.make_qr(text)
+ except Exception:
+ return False, ['Text is too long for a Micro QR code.']
buf = io.BytesIO()
if color:
From 8611f955983ea5331eb2ad5b1e5f9986e4ee8470 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 17:25:35 +0200
Subject: [PATCH 13/17] feat(engine): backgrount color
---
app.py | 3 ++-
justfile | 3 ++-
static/js/color_contrast_warning.js | 40 +++++++++++++++++++++++++++++
static/js/form_remove_image.js | 2 +-
templates/index.html | 17 ++++++++++--
utils/qr_generator.py | 8 +++---
6 files changed, 63 insertions(+), 10 deletions(-)
create mode 100644 static/js/color_contrast_warning.js
diff --git a/app.py b/app.py
index e720001..95fc305 100644
--- a/app.py
+++ b/app.py
@@ -44,10 +44,11 @@ def generate():
raw_image = request.files.get('image')
image = raw_image if (raw_image and raw_image.filename) else None
color = request.form.get('color') or None
+ bg_color = request.form.get('bg_color') or '#ffffff'
micro = request.form.get('micro') == 'on'
generator = QRCodeGenerator()
- success, errors = generator.generate_qr(text, size, image, color, micro)
+ success, errors = generator.generate_qr(text, size, image, color, bg_color, micro)
if not success:
return jsonify({'success': False, 'errors': errors})
diff --git a/justfile b/justfile
index c6bb27b..175561f 100644
--- a/justfile
+++ b/justfile
@@ -11,8 +11,9 @@ help:
install:
uv sync
-# Run all linters (ruff check, ty, codespell, bandit)
+# Run all linters (ruff format --check, ruff check, ty, codespell, bandit)
lint:
+ uv run ruff format --check .
uv run ruff check .
uv run ty check
uv run codespell
diff --git a/static/js/color_contrast_warning.js b/static/js/color_contrast_warning.js
new file mode 100644
index 0000000..6276c78
--- /dev/null
+++ b/static/js/color_contrast_warning.js
@@ -0,0 +1,40 @@
+function hexToRgb(hex) {
+ const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return m ? { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) } : null;
+}
+
+function relativeLuminance(r, g, b) {
+ return [r, g, b].reduce((acc, c, i) => {
+ c /= 255;
+ const lin = c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
+ return acc + lin * [0.2126, 0.7152, 0.0722][i];
+ }, 0);
+}
+
+function contrastRatio(hex1, hex2) {
+ const c1 = hexToRgb(hex1);
+ const c2 = hexToRgb(hex2);
+ if (!c1 || !c2) return 21;
+ const l1 = relativeLuminance(c1.r, c1.g, c1.b);
+ const l2 = relativeLuminance(c2.r, c2.g, c2.b);
+ const lighter = Math.max(l1, l2);
+ const darker = Math.min(l1, l2);
+ return (lighter + 0.05) / (darker + 0.05);
+}
+
+const darkInput = document.getElementById('color');
+const lightInput = document.getElementById('bg_color');
+const contrastWarning = document.getElementById('contrast-warning');
+const imageInput = document.getElementById('image');
+const imageWarning = document.getElementById('image-warning');
+
+function updateContrastWarning() {
+ contrastWarning.classList.toggle('d-none', contrastRatio(darkInput.value, lightInput.value) >= 3);
+}
+
+darkInput.addEventListener('input', updateContrastWarning);
+lightInput.addEventListener('input', updateContrastWarning);
+
+imageInput.addEventListener('change', () => {
+ imageWarning.classList.toggle('d-none', imageInput.files.length === 0);
+});
diff --git a/static/js/form_remove_image.js b/static/js/form_remove_image.js
index 930e342..ec5da56 100644
--- a/static/js/form_remove_image.js
+++ b/static/js/form_remove_image.js
@@ -1,5 +1,5 @@
function clearFileInput() {
var fileInput = document.getElementById('image');
-
fileInput.value = '';
+ fileInput.dispatchEvent(new Event('change'));
}
diff --git a/templates/index.html b/templates/index.html
index c76b0da..aa43fad 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -30,16 +30,28 @@
QR Code Generator
-
+
+
+
+
+
+
+
+ Low contrast between foreground and background colors may make the QR code unreadable.
+
+
@@ -70,6 +82,7 @@
Error
+
diff --git a/utils/qr_generator.py b/utils/qr_generator.py
index d1cebec..8421031 100644
--- a/utils/qr_generator.py
+++ b/utils/qr_generator.py
@@ -18,6 +18,7 @@ def generate_qr(
size: int,
image: FileStorage | None,
color: str | None = None,
+ bg_color: str = '#ffffff',
micro: bool = False,
) -> tuple[bool, list[str]]:
valid, errors = self.validate_data(text, size, image)
@@ -30,10 +31,7 @@ def generate_qr(
return False, ['Text is too long for a Micro QR code.']
buf = io.BytesIO()
- if color:
- qr.save(buf, kind='png', scale=size, dark=color, light='white')
- else:
- qr.save(buf, kind='png', scale=size)
+ qr.save(buf, kind='png', scale=size, dark=color or 'black', light=bg_color)
buf.seek(0)
self.qr_png = buf.read()
@@ -41,7 +39,7 @@ def generate_qr(
ext = 'gif' if image.filename.rsplit('.', 1)[-1].lower() == 'gif' else 'png'
image.seek(0)
art_buf = io.BytesIO()
- qr.to_artistic(background=image, target=art_buf, kind=ext, scale=size)
+ qr.to_artistic(background=image, target=art_buf, kind=ext, scale=size, dark=color or 'black', light=bg_color)
art_buf.seek(0)
self.artistic_png = art_buf.read()
self.artistic_ext = ext
From 5a36842aedbb4daf0f9be434ef82efbd999244b4 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 17:39:54 +0200
Subject: [PATCH 14/17] feat(frontend): homepage button
---
templates/index.html | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/templates/index.html b/templates/index.html
index aa43fad..3ac6953 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -38,14 +38,15 @@
QR Code Generator
-
-
-
-
-
-
-
-
+
From 84751df7870c305da190ecfc7282d2bea036be14 Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 17:40:36 +0200
Subject: [PATCH 15/17] feat(frontend): common fow for colours
---
templates/result.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/templates/result.html b/templates/result.html
index c7e94f4..8d2d5df 100644
--- a/templates/result.html
+++ b/templates/result.html
@@ -23,7 +23,7 @@
QR Code Generated
Download QR Code with Image
{% endif %}
-
Back to previous page
+
â Home
From afb2a08fb3f2e342974c21759971188e4898e78d Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 17:45:16 +0200
Subject: [PATCH 16/17] fix(frontend) minor fixes
---
templates/index.html | 16 +++++++++++-----
templates/result.html | 16 +++++++++++++---
2 files changed, 24 insertions(+), 8 deletions(-)
diff --git a/templates/index.html b/templates/index.html
index 3ac6953..e8a7a00 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -11,6 +11,12 @@
+
+
@@ -38,14 +44,14 @@
QR Code Generator
-
-
+
+
-
+
-
diff --git a/templates/result.html b/templates/result.html
index 8d2d5df..5e6d0d7 100644
--- a/templates/result.html
+++ b/templates/result.html
@@ -10,20 +10,30 @@
crossorigin="anonymous">
+
+
From 82dc9ae253518622a2ab723dfb30c61270abaf1a Mon Sep 17 00:00:00 2001
From: Cybernetic-Ransomware
<71835339+Cybernetic-Ransomware@users.noreply.github.com>
Date: Thu, 4 Jun 2026 18:12:53 +0200
Subject: [PATCH 17/17] fix: minor fixes
---
README.md | 15 ++++++++++++++-
tests/test_mixins.py | 9 +++++++--
tests/test_result_store.py | 11 ++++++++++-
tests/test_routes.py | 7 +++++++
utils/mixins.py | 2 +-
utils/qr_generator.py | 9 +++++----
6 files changed, 44 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index 9db94e9..82dfcb2 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,19 @@ The purpose of this project is to demonstrate how to integrate and test the Segn
## Getting Started:
+### Local development
+
+Requires [uv](https://docs.astral.sh/uv/) and [just](https://just.systems/).
+
+```powershell
+just install # install all dependencies (including dev)
+just runserver # start Flask on http://127.0.0.1:5000/
+just test # run all tests (excluding slow)
+just lint # ruff, ty, codespell, bandit
+just git-precommit # run pre-commit hooks on all files
+```
+
+> **Note:** `docker-compose.yml` mounts the working directory into the container (`.:/app`) for development convenience. Remove or replace the `volumes` section before using it in production.
### Commands reminder:
@@ -38,7 +51,7 @@ minikube tunnel
#### Checks:
```powershell
-kubectl get pods
+kubectl get pods
docker images
```
diff --git a/tests/test_mixins.py b/tests/test_mixins.py
index 598356d..ada3db4 100644
--- a/tests/test_mixins.py
+++ b/tests/test_mixins.py
@@ -1,10 +1,12 @@
import io
from unittest.mock import MagicMock
-from utils.mixins import MAX_SIZE, MIN_SIZE, NotificationMixin
+import pytest
+from utils.mixins import MAX_SIZE, MIN_SIZE, ValidationMixin
-class Validator(NotificationMixin):
+
+class Validator(ValidationMixin):
pass
@@ -20,6 +22,7 @@ def _mock_file(data: bytes, filename: str = 'image.png'):
return f
+@pytest.mark.unit
class TestValidateText:
def test_empty_string_is_valid(self):
ok, msg = v.validate_text('')
@@ -36,6 +39,7 @@ def test_56_chars_is_invalid(self):
assert '55' in msg
+@pytest.mark.unit
class TestValidateSize:
def test_min_boundary(self):
ok, _ = v.validate_size(MIN_SIZE)
@@ -57,6 +61,7 @@ def test_above_max(self):
assert str(MAX_SIZE) in msg
+@pytest.mark.unit
class TestValidateImage:
def test_none_is_valid(self):
ok, msg = v.validate_image(None)
diff --git a/tests/test_result_store.py b/tests/test_result_store.py
index 005f0d6..a46020e 100644
--- a/tests/test_result_store.py
+++ b/tests/test_result_store.py
@@ -1,9 +1,12 @@
import time
from unittest.mock import patch
+import pytest
+
from utils.result_store import ResultStore
+@pytest.mark.unit
def test_put_returns_string():
store = ResultStore()
token = store.put({'data': 1})
@@ -11,12 +14,14 @@ def test_put_returns_string():
assert len(token) > 0
+@pytest.mark.unit
def test_put_returns_unique_tokens():
store = ResultStore()
tokens = {store.put({'n': i}) for i in range(10)}
assert len(tokens) == 10
+@pytest.mark.unit
def test_get_returns_payload():
store = ResultStore()
payload = {'qr': b'bytes', 'x': 42}
@@ -24,11 +29,13 @@ def test_get_returns_payload():
assert store.get(token) == payload
+@pytest.mark.unit
def test_get_unknown_token_returns_none():
store = ResultStore()
assert store.get('nonexistent') is None
+@pytest.mark.unit
def test_get_expired_entry_returns_none():
store = ResultStore(ttl=10)
token = store.put({'data': 1})
@@ -37,6 +44,7 @@ def test_get_expired_entry_returns_none():
assert store.get(token) is None
+@pytest.mark.unit
def test_fifo_eviction_at_cap():
store = ResultStore(max_entries=3)
t1 = store.put({'n': 1})
@@ -46,7 +54,8 @@ def test_fifo_eviction_at_cap():
assert store.get(t1) is None
-def test_no_duplicate_handler_on_reconfigure():
+@pytest.mark.unit
+def test_eviction_keeps_recent_entries():
store = ResultStore(max_entries=2)
t1 = store.put({'n': 1})
t2 = store.put({'n': 2})
diff --git a/tests/test_routes.py b/tests/test_routes.py
index 133e398..522d621 100644
--- a/tests/test_routes.py
+++ b/tests/test_routes.py
@@ -1,13 +1,17 @@
import io
+import pytest
+
from app import store
+@pytest.mark.integration
def test_index(client):
response = client.get('/')
assert response.status_code == 200
+@pytest.mark.integration
class TestGenerate:
def test_valid_text_and_size(self, client):
response = client.post('/generate', data={'text': 'hello', 'size': '10'})
@@ -43,6 +47,7 @@ def test_size_out_of_range(self, client):
assert data['success'] is False
+@pytest.mark.integration
class TestResult:
def test_valid_token(self, client, qr_token):
response = client.get(f'/result?id={qr_token}')
@@ -57,6 +62,7 @@ def test_missing_id_returns_404(self, client):
assert response.status_code == 404
+@pytest.mark.integration
class TestDownload:
def test_valid_token_returns_png(self, client, qr_token):
response = client.get(f'/download?id={qr_token}')
@@ -72,6 +78,7 @@ def test_missing_id_returns_404(self, client):
assert response.status_code == 404
+@pytest.mark.integration
class TestDownloadWithImage:
def test_token_without_artistic_returns_404(self, client, qr_token):
response = client.get(f'/download_with_image?id={qr_token}')
diff --git a/utils/mixins.py b/utils/mixins.py
index 840fd19..e3414e2 100644
--- a/utils/mixins.py
+++ b/utils/mixins.py
@@ -4,7 +4,7 @@
MAX_SIZE = 30
-class NotificationMixin:
+class ValidationMixin:
@staticmethod
def validate_text(text: str) -> tuple[bool, str]:
if len(text) > 55:
diff --git a/utils/qr_generator.py b/utils/qr_generator.py
index 8421031..6ae779d 100644
--- a/utils/qr_generator.py
+++ b/utils/qr_generator.py
@@ -3,10 +3,10 @@
import segno
from werkzeug.datastructures import FileStorage
-from utils.mixins import NotificationMixin
+from utils.mixins import ValidationMixin
-class QRCodeGenerator(NotificationMixin):
+class QRCodeGenerator(ValidationMixin):
def __init__(self) -> None:
self.qr_png: bytes | None = None
self.artistic_png: bytes | None = None
@@ -27,8 +27,9 @@ def generate_qr(
try:
qr = segno.make(text, micro=True) if micro else segno.make_qr(text)
- except Exception:
- return False, ['Text is too long for a Micro QR code.']
+ except segno.DataOverflowError:
+ msg = 'Text is too long for a Micro QR code.' if micro else 'Text is too long for a QR code.'
+ return False, [msg]
buf = io.BytesIO()
qr.save(buf, kind='png', scale=size, dark=color or 'black', light=bg_color)