Compare commits
40 Commits
847cac4bba
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c4ac44271a | |||
| 4b0057859e | |||
| 78066b77ae | |||
| 491a7154e2 | |||
| a7d6d45a78 | |||
| 994624af46 | |||
| b687a260c5 | |||
| a2722d5f9f | |||
| 9efccccc21 | |||
| a11f1a6c38 | |||
| ef82152b95 | |||
| 0e663b95af | |||
| a1bb50087b | |||
| bfe40a4837 | |||
| 1df2ecbebf | |||
| 4aa6ac97fd | |||
| 4248da98fc | |||
| fb5336ef9e | |||
| 67f24c2a8a | |||
| 8fe744afe1 | |||
| 70fa1168ea | |||
| 726c095af5 | |||
| 1f0a27f3af | |||
| 7980a112a3 | |||
| d9330ec8ac | |||
| c8500a4337 | |||
| 03d823c713 | |||
| 773f8ad2b6 | |||
| 3291fbf6a0 | |||
| 84f128806c | |||
| 494170e2ab | |||
| a190471b44 | |||
| 02ecfa2209 | |||
| 112459964a | |||
| 457418c271 | |||
| 1926382021 | |||
| 65b2abdad6 | |||
| 81daf2aa0c | |||
| 3812dd5d47 | |||
| e1130fa493 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
config.php
|
||||
test.php
|
||||
melly-to-grist/.env
|
||||
*/__pycache__
|
||||
new-registration-app/database.db
|
||||
.env
|
||||
**/__pycache__
|
||||
database.db*
|
||||
.idea
|
||||
db_fixtures
|
||||
backups
|
||||
4
README.md
Normal file
4
README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Allmende Essen
|
||||
|
||||
Management App für das gemeinsame Kochen und Essen in der Allmende
|
||||
|
||||
43
alembic.ini
Normal file
43
alembic.ini
Normal file
@@ -0,0 +1,43 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = sqlite:///database.db
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
9
justfile
Normal file
9
justfile
Normal file
@@ -0,0 +1,9 @@
|
||||
lint:
|
||||
uv run isort src
|
||||
uv run black src
|
||||
|
||||
reset_db:
|
||||
rm -f database.db
|
||||
touch database.db
|
||||
alembic upgrade heads
|
||||
bash -c 'shopt -s nullglob; for file in db_fixtures/*.sql; do sqlite3 database.db < "$file"; done'
|
||||
@@ -1 +0,0 @@
|
||||
3.13
|
||||
@@ -1,75 +0,0 @@
|
||||
from collections import defaultdict
|
||||
import sys
|
||||
import openpyxl
|
||||
from pygrister.api import GristApi
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import datetime
|
||||
load_dotenv()
|
||||
|
||||
|
||||
config = {
|
||||
"GRIST_API_SERVER": "allmende-gufi.de",
|
||||
"GRIST_TEAM_SITE": "grist",
|
||||
"GRIST_API_KEY": os.getenv("GRIST_API_KEY"),
|
||||
"GRIST_DOC_ID": "xmEcaq5pvxUB3mBfEY8BLe"
|
||||
}
|
||||
|
||||
# grist = GristApi(config=config)
|
||||
#
|
||||
# result = grist.add_records("Transactions", [{"Datum": "01.01.1900", "Typ": "Essen", "Partei_Konto": "Heinz", "Betrag": 10}])
|
||||
# print(result)
|
||||
def read_excel_file(file_path):
|
||||
# Load the workbook
|
||||
workbook = openpyxl.load_workbook(file_path)
|
||||
|
||||
sheet = workbook["Liste"]
|
||||
|
||||
date = sheet["A1"].value.split()[-1]
|
||||
|
||||
result = {
|
||||
"date": date,
|
||||
"adult": defaultdict(lambda: 0),
|
||||
"children": defaultdict(lambda: 0),
|
||||
}
|
||||
# Loop through each row in the sheet
|
||||
for row in sheet.iter_rows(values_only=True):
|
||||
if row[0] == "Essensanmeldung":
|
||||
if row[2] == "Erwachsene Portionen":
|
||||
result["adult"][row[3]] += row[5]
|
||||
if row[2] == "Kinderportionen (ab 7 Jahren)":
|
||||
result["children"][row[3]] += row[5]
|
||||
|
||||
# Close the workbook
|
||||
workbook.close()
|
||||
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
filename = sys.argv[1]
|
||||
entries = read_excel_file(filename)
|
||||
|
||||
grist = GristApi(config=config)
|
||||
records = []
|
||||
|
||||
today = datetime.date.today().isoformat()
|
||||
|
||||
for name, number in entries["adult"].items():
|
||||
records.append({
|
||||
"Datum": entries["date"],
|
||||
"Typ": "Essen",
|
||||
"Partei_Konto": name,
|
||||
"Betrag": -3.5 * number,
|
||||
"Date_Added": today
|
||||
})
|
||||
for name, number in entries["children"].items():
|
||||
records.append({
|
||||
"Datum": entries["date"],
|
||||
"Typ": "Essen Kind",
|
||||
"Partei_Konto": name,
|
||||
"Betrag": -2 * number,
|
||||
"Date_Added": today
|
||||
})
|
||||
|
||||
grist.add_records("Transactions", records)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
[project]
|
||||
name = "melly-to-grist"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"openpyxl>=3.1.5",
|
||||
"pygrister>=0.7.0",
|
||||
"python-dotenv>=1.1.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"black>=25.1.0",
|
||||
]
|
||||
381
melly-to-grist/uv.lock
generated
381
melly-to-grist/uv.lock
generated
@@ -1,381 +0,0 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "25.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.4.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
|
||||
]
|
||||
|
||||
[[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "et-xmlfile"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
|
||||
]
|
||||
|
||||
[[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "melly-to-grist"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "openpyxl" },
|
||||
{ name = "pygrister" },
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "black" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "openpyxl", specifier = ">=3.1.5" },
|
||||
{ name = "pygrister", specifier = ">=0.7.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "black", specifier = ">=25.1.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openpyxl"
|
||||
version = "3.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "et-xmlfile" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygrister"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2b/5f/b99618eff4ae44ccdf40f0d2d6a298dd6c80bed95f76c13907021f1b2295/pygrister-0.7.0.tar.gz", hash = "sha256:fac4b982857febb7ed995e72ba8cbf7aa3168e8847e443d3f85e7dff9247add4", size = 25584 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/67/b20cbae81094e5f2553d2b1e2d93d644562b0010d999bf0408d47c8e42ee/pygrister-0.7.0-py3-none-any.whl", hash = "sha256:324bfb283a5103ea7a813465bf0f18636088cc3c2da2dc85534e6fe8ee0f5867", size = 18452 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/1a/5f36851f439884bcfe8539f6a20ff7516e7b60f319bbaf69a90dc35cc2eb/typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c", size = 101641 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
|
||||
]
|
||||
@@ -1,190 +0,0 @@
|
||||
import locale
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Union
|
||||
|
||||
import starlette.status as status
|
||||
from fastapi import Depends, FastAPI, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from models import Event, Household, Registration, TeamRegistration
|
||||
|
||||
sqlite_file_name = "database.db"
|
||||
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||
|
||||
connect_args = {"check_same_thread": False}
|
||||
engine = create_engine(sqlite_url, connect_args=connect_args)
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
|
||||
|
||||
|
||||
def get_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
def create_db_and_tables():
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def on_startup(app_: FastAPI):
|
||||
create_db_and_tables()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=on_startup)
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def read_root(request: Request, session: SessionDep):
|
||||
statement = select(Event).order_by(Event.event_time)
|
||||
events = session.exec(statement).all()
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="index.html",
|
||||
context={"events": events, "current_page": "home", "now": datetime.now()},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/event/add")
|
||||
async def add_event_form(request: Request, session: SessionDep):
|
||||
return templates.TemplateResponse(request=request, name="add_event.html")
|
||||
|
||||
|
||||
@app.post("/event/add")
|
||||
async def add_event(request: Request, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
|
||||
event_time = datetime.fromisoformat(form_data["eventTime"])
|
||||
registration_deadline = form_data.get("registrationDeadline")
|
||||
if not registration_deadline:
|
||||
# Find the last Sunday before event_time
|
||||
deadline = event_time
|
||||
while deadline.weekday() != 6: # 6 represents Sunday
|
||||
deadline = deadline.replace(day=deadline.day - 1)
|
||||
registration_deadline = deadline.replace(
|
||||
hour=19, minute=30, second=0, microsecond=0
|
||||
)
|
||||
else:
|
||||
registration_deadline = datetime.fromisoformat(registration_deadline)
|
||||
|
||||
event = Event(
|
||||
title=form_data["eventName"],
|
||||
event_time=event_time,
|
||||
registration_deadline=registration_deadline,
|
||||
description=form_data.get("eventDescription"),
|
||||
recipe_link=form_data.get("recipeLink"),
|
||||
)
|
||||
session.add(event)
|
||||
session.commit()
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}")
|
||||
async def read_event(request: Request, event_id: int, session: SessionDep):
|
||||
statement = select(Event).where(Event.id == event_id)
|
||||
event = session.exec(statement).one()
|
||||
|
||||
statement = select(Household)
|
||||
households = session.exec(statement).all()
|
||||
|
||||
# filter out households with existing registrations
|
||||
households = [
|
||||
h
|
||||
for h in households
|
||||
if h.id not in [reg.household_id for reg in event.registrations]
|
||||
]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="event.html",
|
||||
context={"event": event, "households": households},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/event/{event_id}/register")
|
||||
async def add_registration(request: Request, event_id: int, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
|
||||
# TODO: Make this return a nicer error message
|
||||
try:
|
||||
num_adult_meals = int(form_data["numAdults"]) if form_data["numAdults"] else 0
|
||||
num_children_meals = int(form_data["numKids"]) if form_data["numKids"] else 0
|
||||
num_small_children_meals = (
|
||||
int(form_data["numSmallKids"]) if form_data["numSmallKids"] else 0
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError("All number fields must be integers")
|
||||
|
||||
registration = Registration(
|
||||
household_id=form_data["household"],
|
||||
event_id=event_id,
|
||||
num_adult_meals=num_adult_meals,
|
||||
num_children_meals=num_children_meals,
|
||||
num_small_children_meals=num_small_children_meals,
|
||||
)
|
||||
session.add(registration)
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/registration/{household_id}/delete")
|
||||
async def delete_registration(
|
||||
request: Request, event_id: int, household_id: int, session: SessionDep
|
||||
):
|
||||
"""
|
||||
Deletes a registration record for a specific household at a given event. This endpoint
|
||||
handles the removal of the registration, commits the change to the database, and
|
||||
redirects the user to the event page.
|
||||
"""
|
||||
statement = select(Registration).where(
|
||||
Registration.household_id == household_id, Registration.event_id == event_id
|
||||
)
|
||||
session.delete(session.exec(statement).one())
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.post("/event/{event_id}/register_team")
|
||||
async def add_team_registration(request: Request, event_id: int, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
|
||||
person = form_data["personName"].strip()
|
||||
work_type = form_data["workType"]
|
||||
|
||||
statement = select(TeamRegistration).where(
|
||||
TeamRegistration.person_name == person, TeamRegistration.work_type == work_type
|
||||
)
|
||||
# if the person has already registered for the same work type, just ignore
|
||||
if session.exec(statement).one_or_none() is None:
|
||||
registration = TeamRegistration(
|
||||
person_name=person,
|
||||
event_id=event_id,
|
||||
work_type=form_data["workType"],
|
||||
)
|
||||
TeamRegistration.model_validate(registration)
|
||||
session.add(registration)
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/register_team/{entry_id}/delete")
|
||||
async def delete_team_registration(
|
||||
request: Request,
|
||||
event_id: int,
|
||||
entry_id: int,
|
||||
session: SessionDep,
|
||||
):
|
||||
statement = select(TeamRegistration).where(TeamRegistration.id == entry_id)
|
||||
session.delete(session.exec(statement).one())
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
@@ -1,81 +0,0 @@
|
||||
import typing
|
||||
from datetime import datetime
|
||||
|
||||
from sqlmodel import Field, Relationship, SQLModel, String
|
||||
|
||||
WorkTypes = typing.Literal["cooking", "dishes", "tables"]
|
||||
|
||||
|
||||
class Event(SQLModel, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
title: str = Field(nullable=False)
|
||||
event_time: datetime = Field(nullable=False)
|
||||
registration_deadline: datetime = Field(nullable=False)
|
||||
description: str
|
||||
recipe_link: str
|
||||
|
||||
# Min and max number of people needed for cooking, doing the dishes and preparing the tables
|
||||
team_cooking_min: int = 3
|
||||
team_cooking_max: int = 5
|
||||
|
||||
team_dishes_min: int = 3
|
||||
team_dishes_max: int = 5
|
||||
|
||||
# Todo: Rename to "table"
|
||||
team_prep_min: int = 1
|
||||
team_prep_max: int = 1
|
||||
|
||||
registrations: list["Registration"] = Relationship()
|
||||
team: list["TeamRegistration"] = Relationship()
|
||||
|
||||
def team_min_reached(self, work_type: WorkTypes):
|
||||
threshold = {
|
||||
"cooking": self.team_cooking_min,
|
||||
"dishes": self.team_dishes_min,
|
||||
"tables": self.team_prep_min,
|
||||
}[work_type]
|
||||
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
|
||||
|
||||
def team_max_reached(self, work_type: WorkTypes):
|
||||
threshold = {
|
||||
"cooking": self.team_cooking_max,
|
||||
"dishes": self.team_dishes_max,
|
||||
"tables": self.team_prep_max,
|
||||
}[work_type]
|
||||
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
|
||||
|
||||
def all_teams_min(self):
|
||||
return all(
|
||||
self.team_min_reached(work_type) for work_type in typing.get_args(WorkTypes)
|
||||
)
|
||||
|
||||
def all_teams_max(self):
|
||||
return all(
|
||||
self.team_max_reached(work_type) for work_type in typing.get_args(WorkTypes)
|
||||
)
|
||||
|
||||
|
||||
class TeamRegistration(SQLModel, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
event_id: int | None = Field(default=None, foreign_key="event.id")
|
||||
person_name: str = Field(nullable=False)
|
||||
work_type: WorkTypes = Field(nullable=False, sa_type=String)
|
||||
comment: str | None
|
||||
|
||||
|
||||
class Household(SQLModel, table=True):
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
name: str = Field(nullable=False)
|
||||
|
||||
|
||||
class Registration(SQLModel, table=True):
|
||||
event_id: int | None = Field(default=None, foreign_key="event.id", primary_key=True)
|
||||
household_id: int | None = Field(
|
||||
default=None, foreign_key="household.id", primary_key=True
|
||||
)
|
||||
num_adult_meals: int
|
||||
num_children_meals: int
|
||||
num_small_children_meals: int
|
||||
comment: str | None
|
||||
|
||||
household: Household = Relationship()
|
||||
@@ -1,16 +0,0 @@
|
||||
[project]
|
||||
name = "new-registration-app"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.116.0",
|
||||
"sqlmodel>=0.0.24",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"black>=25.1.0",
|
||||
"isort>=6.0.1",
|
||||
]
|
||||
@@ -1,39 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-4">
|
||||
<div class="col-md-8">
|
||||
<h2>Neues Event erstellen</h2>
|
||||
<form method="post" action="/event/add">
|
||||
<div class="mb-3">
|
||||
<label for="eventName" class="form-label">Event Name</label>
|
||||
<input type="text" class="form-control" id="eventName" name="eventName" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="eventTime" class="form-label">Datum und Uhrzeit</label>
|
||||
<input type="datetime-local" class="form-control" id="eventTime" name="eventTime" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="registrationDeadline" class="form-label">Anmeldungs-Deadline</label>
|
||||
<input type="datetime-local" class="form-control" id="registrationDeadline" name="registrationDeadline">
|
||||
<small class="form-text text-muted">Leer lassen für Sonntag Abend vor dem Event</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="recipeLink" class="form-label">Rezept-Link</label>
|
||||
<input type="text" class="form-control" id="recipeLink" name="recipeLink">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="eventDescription" class="form-label">Beschreibung</label>
|
||||
<textarea class="form-control" id="eventDescription" name="eventDescription" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Event erstellen</button>
|
||||
<a href="/new-registration-app/static" class="btn btn-secondary">Abbrechen</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,27 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row mt-4 mb-3">
|
||||
<div class="col d-flex justify-content-between align-items-center">
|
||||
<h2>Kommende Events</h2>
|
||||
<a href="/event/add" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Neues Event erstellen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
{% for event in events %}
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-1">{{ event.title }}</h5>
|
||||
<p class="text-muted mb-3"><i class="bi bi-calendar"></i> {{ event.event_time.strftime('%A, %d.%m.%Y')
|
||||
}}
|
||||
</p>
|
||||
<p class="card-text">{{ event.description }}</p>
|
||||
<a href="event/{{ event.id }}" class="btn btn-sm {% if event.registration_deadline > now %}btn-primary{% else %}btn-secondary{% endif %}">{% if event.registration_deadline > now %}Zur Anmeldung{% else %}Details ansehen{% endif %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
51
pyproject.toml
Normal file
51
pyproject.toml
Normal file
@@ -0,0 +1,51 @@
|
||||
[project]
|
||||
name = "meal-manager"
|
||||
version = "0.2.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = "~=3.13.0"
|
||||
dependencies = [
|
||||
"alembic>=1.17.0",
|
||||
"fastapi[standard]>=0.116.0",
|
||||
"pygrister>=0.8.0",
|
||||
"python-dotenv>=1.1.1",
|
||||
"reportlab>=4.4.4",
|
||||
"sqlalchemy>=2.0.44",
|
||||
"uvicorn[standard]>=0.35.0",
|
||||
]
|
||||
[build-system]
|
||||
requires = ["uv_build>=0.9.0,<0.10.0"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"black>=25.1.0",
|
||||
"isort>=6.0.1",
|
||||
"pytest>=8.4.2",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
apply-subscriptions = "meal_manager.scripts:apply_subscriptions_cli"
|
||||
meal-manager-nightly = "meal_manager.scripts:run_nightly_tasks"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[tool.alembic]
|
||||
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = "%(here)s/src/meal_manager/alembic"
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
file_template = "%%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s"
|
||||
|
||||
# additional paths to be prepended to sys.path. defaults to the current working directory.
|
||||
prepend_sys_path = [
|
||||
"."
|
||||
]
|
||||
BIN
signup/Logo.png
BIN
signup/Logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,98 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Termin hinzufügen</title>
|
||||
<link rel="stylesheet" href="style.css"> <!-- Link to the CSS file -->
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<div id="branding">
|
||||
<img src="Logo.png" alt="Logo">
|
||||
<h1>Termin hinzufügen</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h2>Neuen Termin hinzufügen</h2>
|
||||
<form action="add.php" method="POST">
|
||||
<label for="title">Title:</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
|
||||
<label for="melly">Melly Link:</label>
|
||||
<input type="text" id="melly" name="melly" required>
|
||||
|
||||
<label for="date">Event Date:</label>
|
||||
<input type="date" id="date" name="date" required>
|
||||
|
||||
<label for="signup_deadline">Anmeldung bis:</label>
|
||||
<input type="datetime-local" id="signup_deadline" name="signup_deadline" required>
|
||||
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<button type="submit" class="btn">Hinzfügen</button>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$config = require 'config.php';
|
||||
|
||||
// Database connection parameters
|
||||
$host = $config['db']['host'];
|
||||
$dbname = $config['db']['dbname'];
|
||||
$username = $config['db']['username'];
|
||||
$password = $config['db']['password'];
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
// Retrieve form data
|
||||
$title = $_POST['title'];
|
||||
$melly = $_POST['melly'];
|
||||
$date = $_POST['date'];
|
||||
$deadline = $_POST['signup_deadline'];
|
||||
|
||||
$webform_password = $_POST['password'];
|
||||
|
||||
if($webform_password != $config['webform_password']) {
|
||||
echo 'Invalid Password!';
|
||||
} else {
|
||||
|
||||
try {
|
||||
// Create connection
|
||||
$dsn = "pgsql:host=$host;dbname=$dbname";
|
||||
$pdo = new PDO($dsn, $username, $password);
|
||||
|
||||
// Set error mode to exception for easier debugging
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// SQL query to insert a new dinner option
|
||||
$sql = "INSERT INTO meals (title, link, event_date, registration_closes) VALUES (:title, :melly, :date, :deadline)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
// Bind parameters
|
||||
$stmt->bindParam(':title', $title);
|
||||
$stmt->bindParam(':melly', $melly);
|
||||
$stmt->bindParam(':date', $date);
|
||||
$stmt->bindParam(':deadline', $deadline);
|
||||
|
||||
// Execute the statement
|
||||
$stmt->execute();
|
||||
|
||||
echo '<p class="success">Dinner option added successfully!</p>';
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// Handle connection or query error
|
||||
echo "Error: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +0,0 @@
|
||||
CREATE TABLE meals (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
link TEXT NOT NULL,
|
||||
event_date DATE NOT NULL,
|
||||
registration_closes TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
|
||||
INSERT INTO meals (title, link, event_date, registration_closes)
|
||||
VALUES ('Kidneybohnen Burger mit veganem Coleslaw','https://melly.de/plan/2ZSNYWR37VB8','2025-03-05', '2025-03-02T17:30:30'),
|
||||
('Gemüselasagne mit Salat','hhttps://melly.de/plan/M4XU9XMVM2HP','2025-02-28', '2025-02-23T17:30:30'),
|
||||
RETURNING *;
|
||||
108
signup/index.php
108
signup/index.php
@@ -1,108 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Allmende-Essen</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<div id="branding">
|
||||
<img src="Logo.png" alt="Logo"/>
|
||||
<h1>Gemeinsames Essen in der Allmende</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<?php
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
$config = require 'config.php';
|
||||
|
||||
// Database connection parameters
|
||||
$host = $config['db']['host'];
|
||||
$dbname = $config['db']['dbname'];
|
||||
$username = $config['db']['username'];
|
||||
$password = $config['db']['password'];
|
||||
|
||||
// Create connection
|
||||
$dsn = "pgsql:host=$host;dbname=$dbname";
|
||||
$pdo = new PDO($dsn, $username, $password);
|
||||
|
||||
// Query to fetch future dinner options
|
||||
$sql = "SELECT id, title, link, event_date, registration_closes FROM meals WHERE event_date >= now()::date order by registration_closes < now(), event_date";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
|
||||
|
||||
$today = strtotime(date('Y-m-d'));
|
||||
$now = strtotime(date("Y-m-d H:i:s"));
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<?php // Display dinner options
|
||||
for($i = 0; $i < sizeof($result); $i++) {
|
||||
$row = $result[$i];
|
||||
|
||||
$event_date = strtotime($row["event_date"]);
|
||||
|
||||
$weekday = date("w", $event_date);
|
||||
$date = new DateTimeImmutable($row["event_date"]);
|
||||
|
||||
$end_of_registration = strtotime($row["registration_closes"]);
|
||||
echo '<div class="dinner-option">';
|
||||
echo '<h2>' . $days[$weekday] . " " . $date->format('d.m.Y') .'</h2>';
|
||||
echo '<p>' . htmlspecialchars($row["title"]) . '</p>';
|
||||
if ($end_of_registration > $now) {
|
||||
echo '<a href="' . $row["link"] . '" class="btn">Zur Anmeldung</a>';
|
||||
} else {
|
||||
echo '<a href="' . $row["link"] . '" class="btn btn-grey">Anmeldungen ansehen</a>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
// close container
|
||||
echo '</div>';
|
||||
|
||||
// Query to fetch past dinner options
|
||||
$sql = "SELECT id, title, link, event_date FROM meals WHERE event_date < now()::date order by event_date";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if(sizeof($result) > 0) {
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Vergangene Essen</h2>
|
||||
|
||||
<?php
|
||||
for($i = 0; $i < sizeof($result); $i++) {
|
||||
$row = $result[$i];
|
||||
|
||||
$event_date = strtotime($row["event_date"]);
|
||||
|
||||
$weekday = date("w", $event_date);
|
||||
$date = new DateTimeImmutable($row["event_date"]);
|
||||
|
||||
echo '<div class="dinner-option">';
|
||||
echo '<h2>' . $days[$weekday] . " " . $date->format('d.m.Y') .'</h2>';
|
||||
echo '<p>' . htmlspecialchars($row["title"]) . '</p>';
|
||||
echo '<a href="' . $row["link"] . '" class="btn btn-grey">Anmeldungen ansehen</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
// close container
|
||||
echo '</div>';
|
||||
}
|
||||
?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f4f4;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
header {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding-top: 30px;
|
||||
min-height: 70px;
|
||||
border-bottom: #77aaff 3px solid;
|
||||
}
|
||||
header a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
font-size: 16px;
|
||||
}
|
||||
header ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
header li {
|
||||
float: left;
|
||||
display: inline;
|
||||
padding: 0 20px 0 20px;
|
||||
}
|
||||
header #branding {
|
||||
float: left;
|
||||
}
|
||||
header #branding img {
|
||||
height: 50px;
|
||||
width: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
header #branding h1 {
|
||||
margin: 0;
|
||||
}
|
||||
header nav {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.dinner-option {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.dinner-option h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.btn {
|
||||
background: #77aaff;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.btn-grey {
|
||||
background: #ccc;
|
||||
color: #333;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #5a99d0;
|
||||
}
|
||||
1
src/meal_manager/alembic/README
Normal file
1
src/meal_manager/alembic/README
Normal file
@@ -0,0 +1 @@
|
||||
pyproject configuration, based on the generic configuration.
|
||||
76
src/meal_manager/alembic/env.py
Normal file
76
src/meal_manager/alembic/env.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from meal_manager.models import Base
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
28
src/meal_manager/alembic/script.py.mako
Normal file
28
src/meal_manager/alembic/script.py.mako
Normal file
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,110 @@
|
||||
"""inital revision
|
||||
|
||||
Revision ID: 299a83240036
|
||||
Revises:
|
||||
Create Date: 2025-10-12 20:46:13.452705
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "299a83240036"
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"event",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("event_time", sa.DateTime(), nullable=False),
|
||||
sa.Column("registration_deadline", sa.DateTime(), nullable=False),
|
||||
sa.Column("description", sa.String(), nullable=False),
|
||||
sa.Column("recipe_link", sa.String(), nullable=False),
|
||||
sa.Column("team_cooking_min", sa.Integer(), nullable=False),
|
||||
sa.Column("team_cooking_max", sa.Integer(), nullable=False),
|
||||
sa.Column("team_dishes_min", sa.Integer(), nullable=False),
|
||||
sa.Column("team_dishes_max", sa.Integer(), nullable=False),
|
||||
sa.Column("team_prep_min", sa.Integer(), nullable=False),
|
||||
sa.Column("team_prep_max", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"household",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("name", sa.String(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"registration",
|
||||
sa.Column("event_id", sa.Integer(), nullable=False),
|
||||
sa.Column("household_id", sa.Integer(), nullable=False),
|
||||
sa.Column("num_adult_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("num_children_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("num_small_children_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("comment", sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["event_id"],
|
||||
["event.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["household_id"],
|
||||
["household.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("event_id", "household_id"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"subscription",
|
||||
sa.Column("household_id", sa.Integer(), nullable=False),
|
||||
sa.Column("num_adult_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("num_children_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("num_small_children_meals", sa.Integer(), nullable=False),
|
||||
sa.Column("comment", sa.String(), nullable=True),
|
||||
sa.Column("last_modified", sa.DateTime(), nullable=False),
|
||||
sa.Column("monday", sa.Boolean(), nullable=False),
|
||||
sa.Column("tuesday", sa.Boolean(), nullable=False),
|
||||
sa.Column("wednesday", sa.Boolean(), nullable=False),
|
||||
sa.Column("thursday", sa.Boolean(), nullable=False),
|
||||
sa.Column("friday", sa.Boolean(), nullable=False),
|
||||
sa.Column("saturday", sa.Boolean(), nullable=False),
|
||||
sa.Column("sunday", sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["household_id"],
|
||||
["household.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("household_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"teamregistration",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("event_id", sa.Integer(), nullable=False),
|
||||
sa.Column("person_name", sa.String(), nullable=False),
|
||||
sa.Column("work_type", sa.Text(), nullable=False),
|
||||
sa.Column("comment", sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["event_id"],
|
||||
["event.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("team_registration")
|
||||
op.drop_table("subscription")
|
||||
op.drop_table("registration")
|
||||
op.drop_table("household")
|
||||
op.drop_table("event")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Add subscriptions_applied column to Event
|
||||
|
||||
Revision ID: 13084c5c1f68
|
||||
Revises: 299a83240036
|
||||
Create Date: 2025-10-27 12:25:14.633641
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "13084c5c1f68"
|
||||
down_revision: Union[str, Sequence[str], None] = "299a83240036"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"event",
|
||||
sa.Column(
|
||||
"subscriptions_applied",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default="false",
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"event",
|
||||
sa.Column(
|
||||
"ignore_subscriptions", sa.Boolean(), nullable=False, server_default="true"
|
||||
),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("event", "subscriptions_applied")
|
||||
op.drop_column("event", "ignore_subscriptions")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,49 @@
|
||||
"""add billing info and organizer name to event
|
||||
|
||||
Revision ID: 914ebe23f071
|
||||
Revises: 13084c5c1f68
|
||||
Create Date: 2025-12-12 12:26:13.314293
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "914ebe23f071"
|
||||
down_revision: Union[str, Sequence[str], None] = "13084c5c1f68"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"event",
|
||||
sa.Column(
|
||||
"billed", sa.Boolean(), nullable=False, server_default=sa.text("false")
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"event",
|
||||
sa.Column(
|
||||
"exclude_from_billing",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text("false"),
|
||||
),
|
||||
)
|
||||
op.add_column("event", sa.Column("organizer_name", sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("event", "organizer_name")
|
||||
op.drop_column("event", "exclude_from_billing")
|
||||
op.drop_column("event", "billed")
|
||||
# ### end Alembic commands ###
|
||||
64
src/meal_manager/grist.py
Normal file
64
src/meal_manager/grist.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
from pygrister.api import GristApi
|
||||
|
||||
config = {
|
||||
"GRIST_API_SERVER": "allmende-gufi.de",
|
||||
"GRIST_TEAM_SITE": "grist",
|
||||
"GRIST_API_KEY": os.getenv("GRIST_API_KEY"),
|
||||
"GRIST_DOC_ID": "xmEcaq5pvxUB3mBfEY8BLe",
|
||||
}
|
||||
|
||||
|
||||
def sync_with_grist(event) -> int:
|
||||
"""Writes the event's registrations to Grist.
|
||||
If a registration already exists for a given household, it is not overwritten.
|
||||
Returns the number of new records.
|
||||
"""
|
||||
grist = GristApi(config=config)
|
||||
|
||||
status_code, grist_response = grist.list_records(
|
||||
"Transactions", filter={"meal_manager_event_id": [event.id]}
|
||||
)
|
||||
|
||||
existing_records = set()
|
||||
for entry in grist_response:
|
||||
existing_records.add(entry["Partei_Konto"])
|
||||
|
||||
new_records = []
|
||||
|
||||
today = datetime.date.today().isoformat()
|
||||
for registration in event.registrations:
|
||||
if registration.household.name in existing_records:
|
||||
continue
|
||||
if registration.num_adult_meals > 0:
|
||||
new_records.append(
|
||||
{
|
||||
"Datum": event.event_time.date().isoformat(),
|
||||
"Typ": "Essen",
|
||||
"Partei_Konto": registration.household.name,
|
||||
"Betrag": -3.5 * registration.num_adult_meals,
|
||||
"Date_Added": today,
|
||||
"meal_manager_event_id": event.id,
|
||||
}
|
||||
)
|
||||
if registration.num_children_meals > 0:
|
||||
new_records.append(
|
||||
{
|
||||
"Datum": event.event_time.date().isoformat(),
|
||||
"Typ": "Essen Kind",
|
||||
"Partei_Konto": registration.household.name,
|
||||
"Betrag": -2 * registration.num_children_meals,
|
||||
"Date_Added": today,
|
||||
"meal_manager_event_id": event.id,
|
||||
}
|
||||
)
|
||||
if new_records:
|
||||
grist.add_records("Transactions", new_records)
|
||||
return len(new_records)
|
||||
else:
|
||||
return 0
|
||||
464
src/meal_manager/main.py
Normal file
464
src/meal_manager/main.py
Normal file
@@ -0,0 +1,464 @@
|
||||
import locale
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
from typing import Annotated
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import starlette.status as status
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, Response
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from meal_manager.grist import sync_with_grist
|
||||
from meal_manager.models import (
|
||||
Base,
|
||||
Event,
|
||||
Household,
|
||||
Registration,
|
||||
Subscription,
|
||||
TeamRegistration,
|
||||
)
|
||||
from meal_manager.pdf import build_dinner_overview_pdf
|
||||
|
||||
sqlite_file_name = "database.db"
|
||||
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||
|
||||
connect_args = {"check_same_thread": False}
|
||||
engine = create_engine(sqlite_url, connect_args=connect_args)
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
|
||||
|
||||
|
||||
def get_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
def get_user(request: Request, allow_none: bool = True) -> dict | None:
|
||||
"""
|
||||
Retrieve user information from the incoming request.
|
||||
|
||||
This function attempts to extract user information from the request headers set by ssowat.
|
||||
If allow_none is set to `True`, then a `None` value will be returned if no user information is found, else
|
||||
an exception will be raised, resulting in a 401 Unauthorized response.
|
||||
|
||||
Used in UserDep and StrictUserDep.
|
||||
|
||||
:param request: The incoming HTTP request containing headers and other context
|
||||
information.
|
||||
:param allow_none: Flag indicating whether returning `None` is permitted when
|
||||
user information is not available.
|
||||
:return: A dictionary containing the username if found, or `None` if no user
|
||||
information is available and `allow_none` is `True`.
|
||||
:raises HTTPException: If user information is not found and `allow_none` is
|
||||
`False`.
|
||||
"""
|
||||
if fake_user := os.environ.get("MEAL_MANAGER_FAKE_USER", False):
|
||||
return {"username": "fake_user", "admin": fake_user == "admin"}
|
||||
if "ynh_user" in request.headers:
|
||||
return {
|
||||
"username": request.headers["ynh_user"],
|
||||
# TODO: This should obviously be replaced with a role based check
|
||||
"admin": request.headers["ynh_user"] in ["niklas.m", "martin.k"]
|
||||
}
|
||||
if allow_none:
|
||||
return None
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail="Not logged in")
|
||||
|
||||
|
||||
def create_db_and_tables():
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def on_startup(app_: FastAPI):
|
||||
create_db_and_tables()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=on_startup)
|
||||
app.mount("/static", StaticFiles(directory="src/meal_manager/static"), name="static")
|
||||
|
||||
templates = Jinja2Templates(directory="src/meal_manager/templates")
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
|
||||
UserDep = Annotated[dict, Depends(get_user)]
|
||||
StrictUserDep = Annotated[dict, Depends(partial(get_user, allow_none=False))]
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index(request: Request, session: SessionDep, user: UserDep):
|
||||
"""Displays coming events and a button to register new ones"""
|
||||
now = datetime.now()
|
||||
# TODO: Once we refactored to use SQLAlchemy directly, we can probably do a nicer filtering on the date alone
|
||||
statement = (
|
||||
select(Event)
|
||||
.order_by(Event.event_time)
|
||||
.where(Event.event_time >= now - timedelta(days=1))
|
||||
)
|
||||
events = session.scalars(statement)
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="index.html",
|
||||
context={"events": events, "current_page": "home", "now": now, "user": user},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/robots.txt")
|
||||
async def robots_txt():
|
||||
return FileResponse("src/meal_manager/static/robots.txt", media_type="text/plain")
|
||||
|
||||
|
||||
@app.get("/past_events")
|
||||
async def past_events(request: Request, session: SessionDep):
|
||||
now = datetime.now()
|
||||
# TODO: Once we refactored to use SQLAlchemy directly, we can probably do a nicer filtering on the date alone
|
||||
statement = (
|
||||
select(Event)
|
||||
.order_by(Event.event_time)
|
||||
.where(Event.event_time < now - timedelta(days=1))
|
||||
)
|
||||
events = session.scalars(statement)
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="index.html",
|
||||
context={"events": events, "current_page": "past", "now": now},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/subscribe")
|
||||
async def subscribe(request: Request, session: SessionDep, user: UserDep):
|
||||
statement = select(Household)
|
||||
households = session.scalars(statement)
|
||||
|
||||
subscriptions = session.scalars(select(Subscription)).all()
|
||||
|
||||
# filter out households with existing subscriptions
|
||||
households = [
|
||||
h for h in households if h.id not in [sub.household_id for sub in subscriptions]
|
||||
]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="subscribe.html",
|
||||
context={
|
||||
"households": households,
|
||||
"subscriptions": subscriptions,
|
||||
"user": user,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/subscribe")
|
||||
async def add_subscribe(request: Request, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
# TODO: Make this return a nicer error message
|
||||
try:
|
||||
num_adult_meals = int(form_data["numAdults"]) if form_data["numAdults"] else 0
|
||||
num_children_meals = int(form_data["numKids"]) if form_data["numKids"] else 0
|
||||
num_small_children_meals = (
|
||||
int(form_data["numSmallKids"]) if form_data["numSmallKids"] else 0
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError("All number fields must be integers")
|
||||
|
||||
subscription = Subscription(
|
||||
household_id=form_data["household"],
|
||||
num_adult_meals=num_adult_meals,
|
||||
num_children_meals=num_children_meals,
|
||||
num_small_children_meals=num_small_children_meals,
|
||||
)
|
||||
|
||||
selected_days = form_data.getlist("days")
|
||||
if selected_days:
|
||||
subscription.monday = "1" in selected_days
|
||||
subscription.tuesday = "2" in selected_days
|
||||
subscription.wednesday = "3" in selected_days
|
||||
subscription.thursday = "4" in selected_days
|
||||
subscription.friday = "5" in selected_days
|
||||
subscription.saturday = "6" in selected_days
|
||||
subscription.sunday = "7" in selected_days
|
||||
|
||||
session.add(subscription)
|
||||
session.commit()
|
||||
|
||||
return RedirectResponse(url="/subscribe", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/subscribe/{household_id}/delete")
|
||||
async def delete_subscription(
|
||||
request: Request, session: SessionDep, household_id: int, user: StrictUserDep
|
||||
):
|
||||
|
||||
statement = select(Subscription).where(Subscription.household_id == household_id)
|
||||
sub = session.scalars(statement).one()
|
||||
|
||||
session.delete(sub)
|
||||
session.commit()
|
||||
|
||||
return RedirectResponse(url="/subscribe", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/add")
|
||||
async def add_event_form(request: Request, user: StrictUserDep):
|
||||
return templates.TemplateResponse(request=request, name="add_event.html")
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/edit")
|
||||
async def edit_event_form(
|
||||
request: Request, event_id: int, session: SessionDep, user: StrictUserDep
|
||||
):
|
||||
statement = select(Event).where(Event.id == event_id)
|
||||
event = session.scalars(statement).one()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
context={"event": event, "edit_mode": True},
|
||||
name="add_event.html",
|
||||
)
|
||||
|
||||
|
||||
@app.post("/event/{event_id}/edit")
|
||||
async def edit_event(
|
||||
request: Request, event_id: int, session: SessionDep, user: StrictUserDep
|
||||
):
|
||||
statement = select(Event).where(Event.id == event_id)
|
||||
event = session.scalars(statement).one()
|
||||
|
||||
form_data = await request.form()
|
||||
event_time, registration_deadline = await parse_event_times(form_data)
|
||||
|
||||
event.title = form_data["eventName"]
|
||||
event.event_time = event_time
|
||||
event.registration_deadline = registration_deadline
|
||||
event.description = form_data.get("eventDescription")
|
||||
event.recipe_link = form_data.get("recipeLink")
|
||||
event.ignore_subscriptions = form_data.get("ignoreSubscriptions") == "on"
|
||||
event.organizer_name = form_data.get("organizerName")
|
||||
event.exclude_from_billing = form_data.get("excludeFromBilling") == "on"
|
||||
|
||||
session.commit()
|
||||
|
||||
return RedirectResponse(url=f"/event/{event.id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.post("/event/add")
|
||||
async def add_event(request: Request, session: SessionDep, user: StrictUserDep):
|
||||
form_data = await request.form()
|
||||
|
||||
event_time, registration_deadline = await parse_event_times(form_data)
|
||||
|
||||
event = Event(
|
||||
title=form_data["eventName"],
|
||||
event_time=event_time,
|
||||
registration_deadline=registration_deadline,
|
||||
description=form_data.get("eventDescription"),
|
||||
recipe_link=form_data.get("recipeLink"),
|
||||
ignore_subscriptions=form_data.get("ignoreSubscriptions") == "on",
|
||||
organizer_name=form_data.get("organizerName"),
|
||||
exclude_from_billing=form_data.get("excludeFromBilling") == "on",
|
||||
)
|
||||
session.add(event)
|
||||
session.commit()
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
async def parse_event_times(form_data):
|
||||
event_time = datetime.fromisoformat(form_data["eventTime"])
|
||||
registration_deadline = form_data.get("registrationDeadline")
|
||||
if not registration_deadline:
|
||||
# Find the last Sunday before event_time
|
||||
deadline = event_time
|
||||
while deadline.weekday() != 6: # 6 represents Sunday
|
||||
deadline = deadline.replace(day=deadline.day - 1)
|
||||
registration_deadline = deadline.replace(
|
||||
hour=19, minute=30, second=0, microsecond=0
|
||||
)
|
||||
else:
|
||||
registration_deadline = datetime.fromisoformat(registration_deadline)
|
||||
return event_time, registration_deadline
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/delete")
|
||||
async def delete_event(
|
||||
request: Request, session: SessionDep, event_id: int, user: StrictUserDep
|
||||
):
|
||||
if not user["admin"]:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
statement = select(Event).where(Event.id == event_id)
|
||||
event = session.scalars(statement).one()
|
||||
|
||||
session.delete(event)
|
||||
session.commit()
|
||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}")
|
||||
async def read_event(
|
||||
request: Request,
|
||||
event_id: int,
|
||||
session: SessionDep,
|
||||
user: UserDep,
|
||||
message: str | None = None,
|
||||
):
|
||||
statement = select(Event).where(Event.id == event_id)
|
||||
event = session.scalars(statement).one()
|
||||
|
||||
statement = select(Household)
|
||||
households = session.scalars(statement)
|
||||
|
||||
# filter out households with existing registrations
|
||||
households = [
|
||||
h
|
||||
for h in households
|
||||
if h.id not in [reg.household_id for reg in event.registrations]
|
||||
]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="event.html",
|
||||
context={
|
||||
"event": event,
|
||||
"households": households,
|
||||
"now": datetime.now(),
|
||||
"user": user,
|
||||
"message": message,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/event/{event_id}/register")
|
||||
async def add_registration(request: Request, event_id: int, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
|
||||
# TODO: Make this return a nicer error message
|
||||
try:
|
||||
num_adult_meals = int(form_data["numAdults"]) if form_data["numAdults"] else 0
|
||||
num_children_meals = int(form_data["numKids"]) if form_data["numKids"] else 0
|
||||
num_small_children_meals = (
|
||||
int(form_data["numSmallKids"]) if form_data["numSmallKids"] else 0
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError("All number fields must be integers")
|
||||
|
||||
registration = Registration(
|
||||
household_id=form_data["household"],
|
||||
event_id=event_id,
|
||||
num_adult_meals=num_adult_meals,
|
||||
num_children_meals=num_children_meals,
|
||||
num_small_children_meals=num_small_children_meals,
|
||||
comment=form_data["comment"],
|
||||
)
|
||||
session.add(registration)
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/registration/{household_id}/delete")
|
||||
async def delete_registration(
|
||||
request: Request,
|
||||
event_id: int,
|
||||
household_id: int,
|
||||
session: SessionDep,
|
||||
user: StrictUserDep,
|
||||
):
|
||||
"""
|
||||
Deletes a registration record for a specific household at a given event. This endpoint
|
||||
handles the removal of the registration, commits the change to the database, and
|
||||
redirects the user to the event page.
|
||||
"""
|
||||
statement = select(Registration).where(
|
||||
Registration.household_id == household_id, Registration.event_id == event_id
|
||||
)
|
||||
session.delete(session.scalars(statement).one())
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.post("/event/{event_id}/register_team")
|
||||
async def add_team_registration(request: Request, event_id: int, session: SessionDep):
|
||||
form_data = await request.form()
|
||||
|
||||
person = form_data["personName"].strip()
|
||||
work_type = form_data["workType"]
|
||||
|
||||
statement = select(TeamRegistration).where(
|
||||
TeamRegistration.person_name == person,
|
||||
TeamRegistration.work_type == work_type,
|
||||
TeamRegistration.event_id == event_id,
|
||||
)
|
||||
# if the person has already registered for the same work type, just ignore
|
||||
if session.scalars(statement).one_or_none() is None:
|
||||
registration = TeamRegistration(
|
||||
person_name=person,
|
||||
event_id=event_id,
|
||||
work_type=form_data["workType"],
|
||||
)
|
||||
session.add(registration)
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/register_team/{entry_id}/delete")
|
||||
async def delete_team_registration(
|
||||
request: Request,
|
||||
event_id: int,
|
||||
entry_id: int,
|
||||
session: SessionDep,
|
||||
user: StrictUserDep,
|
||||
):
|
||||
statement = select(TeamRegistration).where(TeamRegistration.id == entry_id)
|
||||
session.delete(session.scalars(statement).one())
|
||||
session.commit()
|
||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/pdf")
|
||||
def get_event_attendance_pdf(event_id: int, session: SessionDep):
|
||||
|
||||
statement = select(Event).where(Event.id == event_id)
|
||||
event = session.scalars(statement).one()
|
||||
|
||||
pdf_buffer = build_dinner_overview_pdf(event)
|
||||
|
||||
headers = {
|
||||
"Content-Disposition": f"inline; filename=attendance_event_{event_id}.pdf"
|
||||
}
|
||||
|
||||
return Response(
|
||||
content=pdf_buffer.getvalue(), media_type="application/pdf", headers=headers
|
||||
)
|
||||
|
||||
|
||||
@app.get("/event/{event_id}/sync_with_grist")
|
||||
def sync_with_grist_route(event_id: int, session: SessionDep, user: StrictUserDep):
|
||||
"""
|
||||
Synchronizes the specified event with Grist and redirects the user.
|
||||
|
||||
This function retrieves the event by its identifier, synchronizes it with Grist,
|
||||
and then redirects the user to the event page with a success message.
|
||||
|
||||
TODO: Error handling
|
||||
"""
|
||||
statement = select(Event).where(Event.id == event_id)
|
||||
event = session.scalars(statement).one()
|
||||
|
||||
entries_written = sync_with_grist(event)
|
||||
|
||||
message = "Es wurden keine Einträge geschrieben."
|
||||
if entries_written > 0:
|
||||
event.billed = True
|
||||
session.commit()
|
||||
message = f"Erfolgreich {entries_written} Einträge geschrieben."
|
||||
return RedirectResponse(
|
||||
url=f"/event/{event_id}?message={quote_plus(message)}",
|
||||
status_code=status.HTTP_302_FOUND,
|
||||
)
|
||||
175
src/meal_manager/models.py
Normal file
175
src/meal_manager/models.py
Normal file
@@ -0,0 +1,175 @@
|
||||
import typing
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
from sqlalchemy.types import Text
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
WorkTypes = typing.Literal["cooking", "dishes", "tables"]
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = "event"
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
title: Mapped[str] = mapped_column(nullable=False)
|
||||
event_time: Mapped[datetime] = mapped_column(nullable=False)
|
||||
registration_deadline: Mapped[datetime] = mapped_column(nullable=False)
|
||||
description: Mapped[str] = mapped_column(nullable=True)
|
||||
recipe_link: Mapped[str] = mapped_column(nullable=True)
|
||||
|
||||
# Min and max number of people needed for cooking, doing the dishes and preparing the tables
|
||||
team_cooking_min: Mapped[int] = mapped_column(default=3, nullable=False)
|
||||
team_cooking_max: Mapped[int] = mapped_column(default=5, nullable=False)
|
||||
|
||||
team_dishes_min: Mapped[int] = mapped_column(default=3, nullable=False)
|
||||
team_dishes_max: Mapped[int] = mapped_column(default=5, nullable=False)
|
||||
|
||||
# Todo: Rename to "table"
|
||||
team_prep_min: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||
team_prep_max: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||
|
||||
subscriptions_applied: Mapped[bool] = mapped_column(default=False, nullable=False)
|
||||
ignore_subscriptions: Mapped[bool] = mapped_column(default=False, nullable=False)
|
||||
|
||||
billed: Mapped[bool] = mapped_column(default=False, nullable=False)
|
||||
exclude_from_billing: Mapped[bool] = mapped_column(default=False, nullable=False)
|
||||
|
||||
organizer_name: Mapped[str] = mapped_column(nullable=True)
|
||||
|
||||
registrations: Mapped[list["Registration"]] = relationship(
|
||||
"Registration", cascade="all, delete"
|
||||
)
|
||||
team: Mapped[list["TeamRegistration"]] = relationship(
|
||||
"TeamRegistration", cascade="all, delete"
|
||||
)
|
||||
|
||||
def team_min_reached(self, work_type: WorkTypes):
|
||||
threshold = {
|
||||
"cooking": self.team_cooking_min,
|
||||
"dishes": self.team_dishes_min,
|
||||
"tables": self.team_prep_min,
|
||||
}[work_type]
|
||||
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
|
||||
|
||||
def team_max_reached(self, work_type: WorkTypes):
|
||||
threshold = {
|
||||
"cooking": self.team_cooking_max,
|
||||
"dishes": self.team_dishes_max,
|
||||
"tables": self.team_prep_max,
|
||||
}[work_type]
|
||||
return sum(1 for t in self.team if t.work_type == work_type) >= threshold
|
||||
|
||||
def all_teams_min(self):
|
||||
return all(
|
||||
self.team_min_reached(work_type) for work_type in typing.get_args(WorkTypes)
|
||||
)
|
||||
|
||||
def all_teams_max(self):
|
||||
return all(
|
||||
self.team_max_reached(work_type) for work_type in typing.get_args(WorkTypes)
|
||||
)
|
||||
|
||||
@property
|
||||
def registration_open(self):
|
||||
return datetime.now() < self.registration_deadline
|
||||
|
||||
@property
|
||||
def in_the_past(self):
|
||||
return datetime.now() > self.event_time
|
||||
|
||||
|
||||
class TeamRegistration(Base):
|
||||
__tablename__ = "teamregistration"
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("event.id"))
|
||||
person_name: Mapped[str] = mapped_column(nullable=False)
|
||||
work_type: Mapped[WorkTypes] = mapped_column(Text, nullable=False)
|
||||
comment: Mapped[str | None] = mapped_column()
|
||||
|
||||
|
||||
class Household(Base):
|
||||
__tablename__ = "household"
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
|
||||
|
||||
class Registration(Base):
|
||||
__tablename__ = "registration"
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("event.id"), primary_key=True)
|
||||
household_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("household.id"), primary_key=True
|
||||
)
|
||||
num_adult_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
num_children_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
num_small_children_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
comment: Mapped[str | None] = mapped_column()
|
||||
|
||||
household: Mapped["Household"] = relationship()
|
||||
|
||||
|
||||
class Subscription(Base):
|
||||
__tablename__ = "subscription"
|
||||
household_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("household.id"), primary_key=True
|
||||
)
|
||||
num_adult_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
num_children_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
num_small_children_meals: Mapped[int] = mapped_column(nullable=False)
|
||||
comment: Mapped[str | None] = mapped_column()
|
||||
|
||||
last_modified: Mapped[datetime] = mapped_column(
|
||||
default=datetime.now, nullable=False
|
||||
)
|
||||
|
||||
monday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
tuesday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
wednesday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
thursday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
friday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
saturday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
sunday: Mapped[bool] = mapped_column(default=True, nullable=False)
|
||||
|
||||
household: Mapped["Household"] = relationship()
|
||||
|
||||
def day_string_de(self) -> str:
|
||||
"""
|
||||
Generates a string representation of selected days in German short form.
|
||||
"""
|
||||
result = []
|
||||
|
||||
if self.monday:
|
||||
result.append("Mo")
|
||||
if self.tuesday:
|
||||
result.append("Di")
|
||||
if self.wednesday:
|
||||
result.append("Mi")
|
||||
if self.thursday:
|
||||
result.append("Do")
|
||||
if self.friday:
|
||||
result.append("Fr")
|
||||
if self.saturday:
|
||||
result.append("Sa")
|
||||
if self.sunday:
|
||||
result.append("So")
|
||||
|
||||
if len(result) < 7:
|
||||
return ", ".join(result)
|
||||
else:
|
||||
return "Alle"
|
||||
|
||||
def valid_on(self, date: date) -> bool:
|
||||
weekday = date.weekday()
|
||||
return {
|
||||
0: self.monday,
|
||||
1: self.tuesday,
|
||||
2: self.wednesday,
|
||||
3: self.thursday,
|
||||
4: self.friday,
|
||||
5: self.saturday,
|
||||
6: self.sunday,
|
||||
}[weekday]
|
||||
128
src/meal_manager/pdf.py
Normal file
128
src/meal_manager/pdf.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from io import BytesIO
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import A4, portrait
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
||||
|
||||
from meal_manager.models import Event
|
||||
|
||||
|
||||
def build_dinner_overview_pdf(event: Event) -> BytesIO:
|
||||
"""Build a PDF with an overview of the event's attendance."""
|
||||
# Create an in-memory PDF
|
||||
buffer = BytesIO()
|
||||
doc = SimpleDocTemplate(
|
||||
buffer,
|
||||
pagesize=portrait(A4),
|
||||
topMargin=30,
|
||||
bottomMargin=30,
|
||||
leftMargin=40,
|
||||
rightMargin=40,
|
||||
)
|
||||
styles = getSampleStyleSheet()
|
||||
elements = []
|
||||
|
||||
# Title
|
||||
title_style = styles["Title"]
|
||||
title_style.fontSize = 16
|
||||
title_style.spaceAfter = 20
|
||||
elements.append(
|
||||
Paragraph(
|
||||
f"Anwesenheitsliste – {event.title} ({event.event_time.date().strftime('%d.%m.%y')})",
|
||||
title_style,
|
||||
)
|
||||
)
|
||||
elements.append(Spacer(1, 20))
|
||||
|
||||
# Team overview section
|
||||
elements.append(Paragraph("Dienste", styles["Heading2"]))
|
||||
elements.append(Spacer(1, 12))
|
||||
|
||||
team_data = [["Team", "Personen"]]
|
||||
team_types = {
|
||||
"Kochen": [r for r in event.team if r.work_type == "cooking"],
|
||||
"Abwaschen": [r for r in event.team if r.work_type == "dishes"],
|
||||
"Tische decken": [r for r in event.team if r.work_type == "tables"],
|
||||
}
|
||||
|
||||
for team_name, registrations in team_types.items():
|
||||
members = ", ".join(r.person_name for r in registrations)
|
||||
team_data.append([team_name, members])
|
||||
|
||||
team_table = Table(team_data, repeatRows=1, colWidths=[100, "*"])
|
||||
team_table.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#E8E8E8")),
|
||||
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#A0A0A0")),
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 10),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 8),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
elements.append(team_table)
|
||||
elements.append(Spacer(1, 25))
|
||||
|
||||
sum_adults = 0
|
||||
sum_children = 0
|
||||
sum_small_children = 0
|
||||
for r in event.registrations:
|
||||
sum_adults += r.num_adult_meals
|
||||
sum_children += r.num_children_meals
|
||||
sum_small_children += r.num_small_children_meals
|
||||
|
||||
# Attendance section
|
||||
elements.append(Paragraph("Teilnehmende", styles["Heading2"]))
|
||||
elements.append(
|
||||
Paragraph(
|
||||
f"Gesamt: {sum_adults} Erwachsene, {sum_children} Kinder, {sum_small_children} Kleinkinder"
|
||||
)
|
||||
)
|
||||
elements.append(Spacer(1, 12))
|
||||
|
||||
# Table header
|
||||
data = [
|
||||
["Haushalt", "Erwachsene", "Kinder >7", "Kinder <7", "Kommentar", "Anwesend?"]
|
||||
]
|
||||
|
||||
# Table rows
|
||||
for r in event.registrations:
|
||||
data.append(
|
||||
[
|
||||
r.household.name,
|
||||
r.num_adult_meals,
|
||||
r.num_children_meals,
|
||||
r.num_small_children_meals,
|
||||
Paragraph(r.comment or ""),
|
||||
"",
|
||||
]
|
||||
)
|
||||
for _ in range(5):
|
||||
data.append([""] * 6)
|
||||
|
||||
# Create table
|
||||
table = Table(data, repeatRows=1, colWidths=[120, 70, 60, 60, "*", 65])
|
||||
table.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#E8E8E8")),
|
||||
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#A0A0A0")),
|
||||
("ALIGN", (1, 1), (-2, -1), "CENTER"),
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 10),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 3),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
elements.append(table)
|
||||
doc.build(elements)
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
116
src/meal_manager/scripts.py
Normal file
116
src/meal_manager/scripts.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import argparse
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from meal_manager.main import engine, sqlite_file_name
|
||||
from meal_manager.models import Event, Registration, Subscription
|
||||
|
||||
|
||||
def backup_db():
|
||||
backup_dir = Path(os.environ.get("MEAL_MANAGER_BACKUP_DIR", None))
|
||||
shutil.copy2(
|
||||
sqlite_file_name,
|
||||
backup_dir / f"{sqlite_file_name}.{datetime.datetime.now().isoformat()}",
|
||||
)
|
||||
# TODO: Delete old backups
|
||||
|
||||
|
||||
def run_nightly_tasks():
|
||||
print("Applying Subscriptions")
|
||||
with Session(engine) as session:
|
||||
apply_subscriptions(session)
|
||||
|
||||
print("Running db backup")
|
||||
backup_db()
|
||||
|
||||
print("Done running nightly tasks.")
|
||||
|
||||
|
||||
def apply_subscriptions(session: Session, event: Event = None, dry_run: bool = False):
|
||||
|
||||
subscriptions = session.scalars(select(Subscription)).all()
|
||||
|
||||
if event is not None:
|
||||
events = [event]
|
||||
else:
|
||||
today = datetime.date.today()
|
||||
query = select(Event).where(
|
||||
~Event.subscriptions_applied,
|
||||
~Event.ignore_subscriptions,
|
||||
func.strftime("%Y-%m-%d", Event.event_time) >= today.strftime("%Y-%m-%d"),
|
||||
func.strftime("%Y-%m-%d", Event.event_time)
|
||||
<= (today + datetime.timedelta(days=7)).strftime("%Y-%m-%d"),
|
||||
)
|
||||
events = session.scalars(query).all()
|
||||
|
||||
if len(events) == 0:
|
||||
print("No events to process")
|
||||
return
|
||||
|
||||
for event in events:
|
||||
if dry_run:
|
||||
print(f"DRY RUN: Would process event {event.title} ({event.id})")
|
||||
else:
|
||||
print(f"Processing event {event.title} ({event.id})")
|
||||
|
||||
print(f"There are {len(subscriptions)} subscriptions to process")
|
||||
relevant_subscriptions = [
|
||||
s for s in subscriptions if s.valid_on(event.event_time.date())
|
||||
]
|
||||
print(f"{len(relevant_subscriptions)} of them are relevant to this event")
|
||||
|
||||
for subscription in relevant_subscriptions:
|
||||
existing_registration = session.scalars(
|
||||
select(Registration).where(
|
||||
Registration.event_id == event.id,
|
||||
Registration.household_id == subscription.household_id,
|
||||
)
|
||||
).one_or_none()
|
||||
if existing_registration:
|
||||
print(
|
||||
f"There is already a registration for {subscription.household.name}. Skipping subscription."
|
||||
)
|
||||
continue
|
||||
reg = Registration(
|
||||
event_id=event.id,
|
||||
household_id=subscription.household_id,
|
||||
num_adult_meals=subscription.num_adult_meals,
|
||||
num_children_meals=subscription.num_children_meals,
|
||||
num_small_children_meals=subscription.num_small_children_meals,
|
||||
comment="Dauerhafte Anmeldung",
|
||||
)
|
||||
if dry_run:
|
||||
print(f"DRY RUN: Would register {subscription.household.name}")
|
||||
else:
|
||||
session.add(reg)
|
||||
print(f"Registered {subscription.household.name}")
|
||||
event.subscriptions_applied = True
|
||||
|
||||
if not dry_run:
|
||||
session.commit()
|
||||
|
||||
|
||||
def apply_subscriptions_cli():
|
||||
parser = argparse.ArgumentParser(description="Apply subscriptions for an event")
|
||||
parser.add_argument("--event_id", type=int, help="Event ID (required)")
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Run without making changes"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Access the arguments
|
||||
event_id = args.event_id
|
||||
dry_run = args.dry_run
|
||||
|
||||
with Session(engine) as session:
|
||||
if event_id is not None:
|
||||
event = session.scalars(select(Event).where(Event.id == event_id)).one()
|
||||
apply_subscriptions(session, event, dry_run)
|
||||
else:
|
||||
apply_subscriptions(session, dry_run=dry_run)
|
||||
29
src/meal_manager/static/css/allmende.css
Normal file
29
src/meal_manager/static/css/allmende.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/* theme.css */
|
||||
/* Green Bootstrap Theme */
|
||||
|
||||
:root {
|
||||
--bs-primary: #198754;
|
||||
--bs-primary-rgb: 25, 135, 84;
|
||||
--bs-success: #198754;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-link-color: var(--bs-primary);
|
||||
--bs-link-hover-color: #146c43;
|
||||
}
|
||||
|
||||
/* Explicit fallback overrides for older versions (<=5.2) */
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #198754;
|
||||
border-color: #198754;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #157347;
|
||||
border-color: #146c43;
|
||||
}
|
||||
.btn-primary:focus,
|
||||
.btn-primary:active {
|
||||
color: #fff;
|
||||
background-color: #146c43;
|
||||
border-color: #125c39;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
2
src/meal_manager/static/robots.txt
Normal file
2
src/meal_manager/static/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
60
src/meal_manager/templates/add_event.html
Normal file
60
src/meal_manager/templates/add_event.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-4">
|
||||
<div class="col-md-8">
|
||||
{% if edit_mode %}<h2>Event bearbeiten</h2>{% else %}<h2>Neues Event erstellen</h2>{% endif %}
|
||||
{% if edit_mode %}<form method="post" action="/event/{{ event.id }}/edit">{% else %}<form method="post" action="/event/add">{% endif %}
|
||||
<div class="mb-3">
|
||||
<label for="eventName" class="form-label">Event Name</label>
|
||||
<input type="text" class="form-control" id="eventName" name="eventName" required {% if edit_mode %}value="{{ event.title }}"{% endif %}>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="eventTime" class="form-label">Datum und Uhrzeit</label>
|
||||
<input type="datetime-local" class="form-control" id="eventTime" name="eventTime" required {% if edit_mode %}value="{{ event.event_time }}"{% endif %}>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="registrationDeadline" class="form-label">Anmeldungs-Deadline</label>
|
||||
<input type="datetime-local" class="form-control" id="registrationDeadline" name="registrationDeadline" {% if edit_mode %}value="{{ event.registration_deadline }}"{% endif %}>
|
||||
<small class="form-text text-muted">Leer lassen für Sonntag Abend vor dem Event</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="organizerName" class="form-label">Organisator*in</label>
|
||||
<input type="text" class="form-control" id="organizerName" name="organizerName" {% if edit_mode %}value="{{ event.organizer_name }}"{% endif %}>
|
||||
<small class="form-text text-muted">Name der Person, die das Event organisiert.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="recipeLink" class="form-label">Rezept-Link</label>
|
||||
<input type="text" class="form-control" id="recipeLink" name="recipeLink" {% if edit_mode %}value="{{ event.recipe_link }}"{% endif %}>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="eventDescription" class="form-label">Beschreibung</label>
|
||||
<textarea class="form-control" id="eventDescription" name="eventDescription" rows="3" {% if edit_mode %}value="{{ event.description }}"{% endif %}></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="ignoreSubscriptions" name="ignoreSubscriptions" {% if (edit_mode and event.ignore_subscriptions) or not edit_mode %}checked{% endif %}>
|
||||
<label class="form-check-label" for="ignoreSubscriptions">Dauerhafte Anmeldung ignorieren</label>
|
||||
<small class="form-text text-muted">
|
||||
Aktivieren, um dauerhafte Anmeldungen für dieses Event zu ignorieren. Das sollte für alle Events getan werden, die keine offiziellen AG Kochen Kochabende sind.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="excludeFromBilling" name="excludeFromBilling" {% if edit_mode and event.exclude_from_billing %}checked{% endif %}>
|
||||
<label class="form-check-label" for="excludeFromBilling">Keine Abrechnung</label>
|
||||
<small class="form-text text-muted">
|
||||
Aktivieren, um dieses Event von der Abrechnung auszuschließen.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Event {% if edit_mode %}bearbeiten{% else %}erstellen{% endif %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -6,6 +6,7 @@
|
||||
<title>Allmende Essen</title>
|
||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/static/icons/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link href="/static/css/allmende.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="p-3 m-0 border-0 bd-example">
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
@@ -21,14 +22,14 @@
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if current_page == 'home' %}active{% endif %}" {% if current_page == 'home' %}aria-current="page"{% endif %} href="/">Home</a>
|
||||
<a class="nav-link {% if current_page == 'home' %}active{% endif %}" {% if current_page == 'home' %}aria-current="page"{% endif %} href="/">Kommende</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if current_page == 'vergangene' %}active{% endif %}" {% if current_page == 'vergangene' %}aria-current="page"{% endif %} href="/vergangene">Vergangene</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if current_page == 'preise' %}active{% endif %}" {% if current_page == 'preise' %}aria-current="page"{% endif %} href="/preise">Preise</a>
|
||||
<a class="nav-link {% if current_page == 'past' %}active{% endif %}" {% if current_page == 'past' %}aria-current="page"{% endif %} href="/past_events">Vergangene</a>
|
||||
</li>
|
||||
<!-- <li class="nav-item">-->
|
||||
<!-- <a class="nav-link {% if current_page == 'preise' %}active{% endif %}" {% if current_page == 'preise' %}aria-current="page"{% endif %} href="/preise">Preise</a>-->
|
||||
<!-- </li>-->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +1,15 @@
|
||||
{% import "macros.j2" as macros %}
|
||||
{% macro teamEntries(event, work_type) -%}
|
||||
{% for entry in event.team | selectattr("work_type", "equalto", work_type)%}
|
||||
<div class="d-inline-flex align-items-center bg-light border rounded-pill px-3 py-1 m-1">
|
||||
<span class="me-2">{{ entry.person_name }}</span>
|
||||
{% if user and event.registration_open -%}
|
||||
<button type="button" class="btn btn-sm p-0 border-0 bg-transparent text-muted">
|
||||
<a href="/event/{{event.id}}/register_team/{{entry.id}}/delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</button>
|
||||
{% endif -%}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
@@ -14,23 +17,74 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<p class="h1">{{ event.title }}</p>
|
||||
<p class="text-muted">{{ event.event_time.strftime('%A, %d.%m.%Y') }}</p>
|
||||
{% if event.organizer_name %}<p>Organisiert von {{ event.organizer_name }}</p>{% endif %}
|
||||
<p>{{ event.description }}</p>
|
||||
<hr class="hr"/>
|
||||
<p class="h3">Anmeldungen</p>
|
||||
{% if message %}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="container">
|
||||
|
||||
<p class="h3">Anmeldungen</p>
|
||||
{% if not user %}
|
||||
<div class="alert alert-warning m-2">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Das Löschen von Anmeldungen und Dienstanmeldungen ist nur möglich, wenn du in der Allmende-Cloud eingeloggt bist. <br/> Geht dazu auf <a href="https://cloud.allmende-gufi.de" target="_blank">allmende.cloud</a> und melde dich an.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row m-2">
|
||||
<div class="col-md-4 d-flex justify-content-center align-items-center flex-column">
|
||||
<p class="text-muted w-100 mb-2">Anmeldung schließt {{ event.registration_deadline.strftime('%A, %d.%m.%Y, %H:%M Uhr') }}</p>
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-primary mb-2 w-100" data-bs-toggle="modal" data-bs-target="#registration">
|
||||
<button type="button" class="btn btn-primary mb-2 w-100" {% if not (event.registration_open or (user and user.admin)) %}disabled{% endif%} data-bs-toggle="modal" data-bs-target="#registration">
|
||||
Anmeldung hinzufügen
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary mb-2 w-100" {% if event.all_teams_max() %}disabled{% endif %} data-bs-toggle="modal" data-bs-target="#teamRegistration">
|
||||
<button type="button" class="btn btn-primary mb-2 w-100" {% if event.all_teams_max() or event.in_the_past %}disabled{% endif %} data-bs-toggle="modal" data-bs-target="#teamRegistration">
|
||||
Dienst übernehmen
|
||||
</button>
|
||||
{% if event.recipe_link %}
|
||||
<a href="{{ event.recipe_link }}" class="btn btn-outline-primary mb-2 w-100" target="_blank">
|
||||
<i class="bi bi-book"></i> Original Rezept ansehen
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="row w-100">
|
||||
<div class="col-6 p-1">
|
||||
<a href="/event/{{event.id}}/pdf" class="btn btn-secondary w-100" target="_blank">
|
||||
<i class="bi bi-printer m-2"></i> Druckansicht
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-6 p-1">
|
||||
{% if user -%}
|
||||
<a href="/event/{{event.id}}/edit" class="btn btn-secondary w-100">
|
||||
<i class="bi bi-pen m-2"></i> Event bearbeiten
|
||||
</a>
|
||||
{% else -%}
|
||||
<button type="button" class="btn btn-secondary w-100" data-bs-toggle="modal" data-bs-target="#loginInfo">
|
||||
<i class="bi bi-pen m-2"></i> Event bearbeiten
|
||||
</button>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{% if user and user.admin %}
|
||||
<div class="row w-100">
|
||||
<div class="col-6 p-1">
|
||||
<button type="button" class="btn btn-danger w-100" data-bs-toggle="modal" data-bs-target="#deleteEvent">
|
||||
Event Löschen
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 p-1">
|
||||
<a href="/event/{{event.id}}/sync_with_grist" class="btn btn-secondary w-100 {% if event.exclude_from_billing %}disabled{% endif %}">
|
||||
<i class="bi bi-cash-coin m-2"></i> Abrechnen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@@ -102,7 +156,8 @@
|
||||
<th scope="col">Erwachsene</th>
|
||||
<th scope="col">Kinder</th>
|
||||
<th scope="col">Kleinkinder</th>
|
||||
<th scope="col">Löschen</th>
|
||||
<th scope="col">Kommentar</th>
|
||||
{% if user %}<th scope="col">Löschen</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -112,7 +167,8 @@
|
||||
<td>{{ reg.num_adult_meals }}</td>
|
||||
<td>{{ reg.num_children_meals }}</td>
|
||||
<td>{{ reg.num_small_children_meals }}</td>
|
||||
<td><a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete"><i class="bi bi-trash"></i></a></td>
|
||||
<td>{% if reg.comment %}{{ reg.comment }}{% endif %}</td>
|
||||
{% if user %}<td><a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete"><i class="bi bi-trash"></i></a></td>{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -126,23 +182,27 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="card-title mb-0">{{ reg.household.name }}</h5>
|
||||
<a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete" class="text-danger">
|
||||
{% if user %}<a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete" class="text-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</a>{% endif -%}
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-4 text-center">
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted small">Erwachsene</div>
|
||||
<div class="fw-bold">{{ reg.num_adult_meals }}</div>
|
||||
</div>
|
||||
<div class="col-4 text-center">
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted small">Kinder</div>
|
||||
<div class="fw-bold">{{ reg.num_children_meals }}</div>
|
||||
</div>
|
||||
<div class="col-4 text-center">
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted small">Kleinkinder</div>
|
||||
<div class="fw-bold">{{ reg.num_small_children_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted small">Kommentar</div>
|
||||
<div class="small">{{ reg.comment }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,15 +222,14 @@
|
||||
<div class="modal-body">
|
||||
<p>Wenn dein Haushalt nicht auswählbar ist, existiert schon eine Anmeldung. Wenn du die Anmeldung ändern willst, lösche die bestehende Anmeldung und lege eine neue an.</p>
|
||||
<div class="mb-3">
|
||||
<select name="household" class="form-select" aria-label="Multiple select example">
|
||||
<option selected>Wer?</option>
|
||||
<select name="household" class="form-select" aria-label="Multiple select example" required>
|
||||
<option value="" disabled selected hidden>Wer?</option>
|
||||
{% for household in households %}
|
||||
<option value="{{household.id}}">{{household.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="InputAdults" class="form-label">Anzahl Erwachsene</label>
|
||||
@@ -187,6 +246,11 @@
|
||||
<input name="numSmallKids" id="InputSmallKids" type="number" class="form-control"
|
||||
aria-label="Anzahl Kinder <7" min="0" step="1" inputmode="numeric">
|
||||
</div>
|
||||
<div class="mb-3 mt-3">
|
||||
<label for="InputComment" class="form-label">Kommentar</label>
|
||||
<input name="comment" id="InputComment" class="form-control"
|
||||
aria-label="Kommentar">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,8 +278,15 @@
|
||||
<div class="col-md-6">
|
||||
<label for="personName" class="form-label">Name</label>
|
||||
<input name="personName" id="personName" type="text" class="form-control"
|
||||
aria-label="Name">
|
||||
aria-label="Name" list="people">
|
||||
</div>
|
||||
<datalist id="people">
|
||||
{% for household in households %}
|
||||
{% for person in household.name.split(",") %}
|
||||
<option value="{{ person.strip() }}">
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
<div class="col-md-6">
|
||||
<label for="workType" class="form-label">Dienst-Art</label>
|
||||
<select id="workType" name="workType" class="form-select" aria-label="Multiple select example">
|
||||
@@ -235,4 +306,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Event Modal -->
|
||||
<div class="modal fade" id="deleteEvent" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
|
||||
aria-labelledby="deleteEventLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h1 class="modal-title fs-5" id="deleteEventLabel">Event endgültig löschen?</h1>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
Diese Aktion kann nicht rückgängig gemacht werden! Alle Anmeldungen und Dienstanmeldungen werden
|
||||
unwiderruflich gelöscht.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="/event/{{event.id}}/delete" class="btn btn-danger">Unwiderruflich Löschen</a>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ macros.login_info_modal() }}
|
||||
|
||||
{% endblock %}
|
||||
60
src/meal_manager/templates/index.html
Normal file
60
src/meal_manager/templates/index.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "macros.j2" as macros %}
|
||||
{% block content %}
|
||||
<div class="row mt-4 mb-3">
|
||||
<div class="col d-flex justify-content-between align-items-center">
|
||||
<h2>{% if current_page == "home" %}Kommende{% else %}Vergangene{% endif %} Kochabende</h2>
|
||||
{% if user %}
|
||||
<a href="/event/add" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Neues Event erstellen
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#loginInfo">
|
||||
<i class="bi bi-plus-circle"></i> Neues Event erstellen
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="card bg-success-subtle text-success-emphasis border-0 shadow-sm text-center">
|
||||
<div class="card-body py-4">
|
||||
<i class="bi bi-calendar-heart fs-3 mb-2"></i>
|
||||
<h5 class="card-title mb-2">Nie wieder die Anmeldung vergessen</h5>
|
||||
<p class="card-text small mb-3">
|
||||
Die Dauerhafte Anmeldung gilt für alle kommenden Kochabende.
|
||||
</p>
|
||||
<a href="/subscribe" class="btn btn-light btn-sm fw-semibold px-3">
|
||||
Jetzt dauerhaft Anmelden
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
|
||||
|
||||
{% for event in events %}
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<h5 class="card-title mb-0">
|
||||
{{ event.title }}
|
||||
{% if event.billed %}<i class="bi bi-cash-coin" title="Abgerechnet"></i>{% endif %}
|
||||
{% if event.exclude_from_billing %}<i class="bi bi-ban" title="Keine Abrechung"></i>{% endif %}
|
||||
</h5>
|
||||
</div>
|
||||
{% if event.organizer_name %}<p class="text-muted small mb-0"><i class="bi bi-person"></i> {{ event.organizer_name }}</p>{% endif %}
|
||||
</div>
|
||||
<p class="text-muted mb-3"><i class="bi bi-calendar"></i> {{ event.event_time.strftime('%A, %d.%m.%Y') }}</p>
|
||||
<p class="card-text">{{ event.description }}</p>
|
||||
<a href="event/{{ event.id }}" class="btn btn-sm {% if event.registration_deadline > now %}btn-primary{% else %}btn-secondary{% endif %}">{% if event.registration_deadline > now %}Zur Anmeldung{% else %}Details ansehen{% endif %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
{{ macros.login_info_modal() }}
|
||||
{% endblock %}
|
||||
19
src/meal_manager/templates/macros.j2
Normal file
19
src/meal_manager/templates/macros.j2
Normal file
@@ -0,0 +1,19 @@
|
||||
{% macro login_info_modal() -%}
|
||||
<div class="modal fade" id="loginInfo" tabindex="-1" aria-labelledby="loginInfoLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="loginInfoLabel">Bitte einloggen</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Einige Funktionen, wie das Erstellen und Bearbeiten von Koch-Events, stehen nur zur Verfügung, wenn du in die Allmende-Cloud
|
||||
eingeloggt bist. Gehe dazu auf <a href="https://cloud.allmende-gufi.de">cloud.allmende-gufi.de</a> und logge dich ein.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Verstanden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
146
src/meal_manager/templates/subscribe.html
Normal file
146
src/meal_manager/templates/subscribe.html
Normal file
@@ -0,0 +1,146 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mt-4">
|
||||
<!-- Left column: subscription form -->
|
||||
<div class="col-12 col-lg-6">
|
||||
{% if not user %}
|
||||
<div class="alert alert-warning m-2">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Das Löschen von dauerhaften Anmeldungen ist nur möglich, wenn du in der Allmende-Cloud eingeloggt bist. <br/> Geht dazu auf <a href="https://cloud.allmende-gufi.de" target="_blank">allmende.cloud</a> und melde dich an.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h1 class="fs-5 mb-0">Dauerhafte Anmeldung zu allen Kochabenden</h1>
|
||||
</div>
|
||||
|
||||
<form action="/subscribe" method="POST">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
Mit einer dauerhaften Anmeldung kannst du dich/euch für alle zukünftigen Kochabende
|
||||
anmelden. Es ist möglich
|
||||
diese Anmeldung auf bestimmte Wochentage zu beschränken.
|
||||
</p>
|
||||
<p>
|
||||
Dauerhafte Anmeldungen werden eine Woche vor einem Kochabend als Anmeldungen für diesen
|
||||
Abend eingetragen. Danach
|
||||
können sie auch noch gelöscht bzw. bearbeitet werden.
|
||||
</p>
|
||||
|
||||
<!-- Info box about 7-day limitation -->
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Hinweis:</strong> Neu angelegte dauerhafte Anmeldungen werden erst nach einer Woche aktiv. Für Kochabende, die in weniger als 7 Tagen stattfinden, musst du dich noch separat anmelden.
|
||||
</div>
|
||||
|
||||
<!-- Household selection -->
|
||||
<div class="mb-3">
|
||||
<select name="household" class="form-select" required>
|
||||
<option value="" disabled selected hidden>Wer?</option>
|
||||
{% for household in households %}
|
||||
<option value="{{household.id}}">{{household.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
Wenn dein Haushalt hier nicht auswählbar ist, besteht bereits eine dauerhafte Anmeldung.
|
||||
Um Änderungen vorzunehmen, lösche die bestehende Anmeldung und lege eine neue an.
|
||||
</p>
|
||||
|
||||
<!-- Person counts -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col">
|
||||
<label for="InputAdults" class="form-label">Anzahl Erwachsene</label>
|
||||
<input name="numAdults" id="InputAdults" type="number" class="form-control"
|
||||
aria-label="Anzahl Erwachsene" min="0" step="1" inputmode="numeric">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="InputKids" class="form-label">Anzahl Kinder >7</label>
|
||||
<input name="numKids" id="InputKids" type="number" class="form-control"
|
||||
aria-label="Anzahl Kinder >7" min="0" step="1" inputmode="numeric">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="InputSmallKids" class="form-label">Anzahl Kinder <7</label>
|
||||
<input name="numSmallKids" id="InputSmallKids" type="number" class="form-control"
|
||||
aria-label="Anzahl Kinder <7" min="0" step="1" inputmode="numeric">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Days of the week -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Wochentage auswählen (optional)</label>
|
||||
<p class="text-muted small mb-2">
|
||||
Wenn du nur an bestimmten Tagen teilnehmen möchtest, wähle sie hier aus.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
{% set days = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag",
|
||||
"Sonntag"] %}
|
||||
{% for day in days %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="days" value="{{loop.index}}"
|
||||
id="day-{{loop.index}}">
|
||||
<label class="form-check-label" for="day-{{loop.index}}">
|
||||
{{day}}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="card-footer d-flex justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-primary">Dauerhaft anmelden</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column: existing registrations -->
|
||||
<div class="col-12 col-lg-6 mt-4 mt-lg-0">
|
||||
<p class="h4 m-2">Bestehende dauerhafte Anmeldungen</p>
|
||||
{% if subscriptions | length == 0 %}
|
||||
<p class="m-2">Es gibt noch keine dauerhaften Anmeldungen</p>
|
||||
{% else %}
|
||||
{% for sub in subscriptions %}
|
||||
<div class="card mb-2">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<h6 class="mb-0">{{ sub.household.name }}</h6>
|
||||
{% if user %}<a href="/subscribe/{{sub.household.id}}/delete" class="text-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>{% endif %}
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted" style="font-size: 0.7rem;">Erwachsene</div>
|
||||
<div class="fw-bold small">{{ sub.num_adult_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted" style="font-size: 0.7rem;">Kinder</div>
|
||||
<div class="fw-bold small">{{ sub.num_children_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted" style="font-size: 0.7rem;">Kleinkinder</div>
|
||||
<div class="fw-bold small">{{ sub.num_small_children_meals }}</div>
|
||||
</div>
|
||||
<div class="col-3 text-center">
|
||||
<div class="text-muted" style="font-size: 0.7rem;">Tage</div>
|
||||
<div class="fw-bold small">{{ sub.day_string_de() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
19
tests/conftest.py
Normal file
19
tests/conftest.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from meal_manager.models import Base
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(bind=engine) # Create tables
|
||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# Provide a session and the engine
|
||||
db = TestingSessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
56
tests/test_nightly_tasks.py
Normal file
56
tests/test_nightly_tasks.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import datetime
|
||||
|
||||
from meal_manager.models import Event, Household, Subscription
|
||||
from meal_manager.scripts import apply_subscriptions
|
||||
|
||||
|
||||
def test_subscriptions(db_session):
|
||||
now = datetime.datetime.now()
|
||||
event1 = Event(
|
||||
title="far future",
|
||||
event_time=now + datetime.timedelta(days=8, hours=2),
|
||||
registration_deadline=now + datetime.timedelta(days=3),
|
||||
description="This event should not be proceesed.",
|
||||
)
|
||||
event2 = Event(
|
||||
title="soon",
|
||||
event_time=now + datetime.timedelta(days=7, hours=2),
|
||||
registration_deadline=now + datetime.timedelta(days=2),
|
||||
description="This event should be proceesed.",
|
||||
)
|
||||
ignore = Event(
|
||||
title="ignore me",
|
||||
event_time=now + datetime.timedelta(days=7, hours=2),
|
||||
description=(
|
||||
"This event should not be proceesed because it "
|
||||
"has ignore_subscriptions set to True."
|
||||
),
|
||||
registration_deadline=now + datetime.timedelta(days=2, hours=2),
|
||||
ignore_subscriptions=True,
|
||||
)
|
||||
db_session.add(event1)
|
||||
db_session.add(event2)
|
||||
db_session.add(ignore)
|
||||
|
||||
db_session.add(Household(name="Klaus", id=1))
|
||||
db_session.add(
|
||||
Subscription(
|
||||
household_id=1,
|
||||
num_adult_meals=2,
|
||||
num_children_meals=1,
|
||||
num_small_children_meals=0,
|
||||
)
|
||||
)
|
||||
db_session.commit()
|
||||
|
||||
assert not event1.subscriptions_applied
|
||||
assert not event2.subscriptions_applied
|
||||
|
||||
apply_subscriptions(db_session)
|
||||
|
||||
assert len(event1.registrations) == 0
|
||||
assert len(event2.registrations) == 1
|
||||
assert len(ignore.registrations) == 0
|
||||
assert not event1.subscriptions_applied
|
||||
assert not ignore.subscriptions_applied
|
||||
assert event2.subscriptions_applied
|
||||
267
new-registration-app/uv.lock → uv.lock
generated
267
new-registration-app/uv.lock → uv.lock
generated
@@ -1,6 +1,20 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
requires-python = "==3.13.*"
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mako" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/45/6f4555f2039f364c3ce31399529dcf48dd60726ff3715ad67f547d87dfd2/alembic-1.17.0.tar.gz", hash = "sha256:4652a0b3e19616b57d652b82bfa5e38bf5dbea0813eed971612671cb9e90c0fe", size = 1975526, upload-time = "2025-10-11T18:40:13.585Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/1f/38e29b06bfed7818ebba1f84904afdc8153ef7b6c7e0d8f3bc6643f5989c/alembic-1.17.0-py3-none-any.whl", hash = "sha256:80523bc437d41b35c5db7e525ad9d908f79de65c27d6a5a5eab6df348a352d99", size = 247449, upload-time = "2025-10-11T18:40:16.288Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -53,6 +67,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.2.1"
|
||||
@@ -173,13 +212,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -243,6 +275,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[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 = "isort"
|
||||
version = "6.0.1"
|
||||
@@ -264,6 +305,18 @@ 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 = "mako"
|
||||
version = "1.3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
@@ -314,39 +367,51 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new-registration-app"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
name = "meal-manager"
|
||||
version = "0.2.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "sqlmodel" },
|
||||
{ name = "pygrister" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "reportlab" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "black" },
|
||||
{ name = "isort" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "alembic", specifier = ">=1.17.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.116.0" },
|
||||
{ name = "sqlmodel", specifier = ">=0.0.24" },
|
||||
{ name = "pygrister", specifier = ">=0.8.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
||||
{ name = "reportlab", specifier = ">=4.4.4" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.44" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "black", specifier = ">=25.1.0" },
|
||||
{ name = "isort", specifier = ">=6.0.1" },
|
||||
{ name = "pytest", specifier = ">=8.4.2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -367,6 +432,39 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.4.0"
|
||||
@@ -376,6 +474,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
|
||||
]
|
||||
|
||||
[[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 = "pydantic"
|
||||
version = "2.11.7"
|
||||
@@ -433,6 +540,35 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygrister"
|
||||
version = "0.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/a2/804b3e63bce91fb0c7f093b6f3e522e476d9f3074cd3384d713f73fff78b/pygrister-0.8.0.tar.gz", hash = "sha256:4faaad23b27c9ae46dc7b321a0de376f3bfbdaa5faa7ffd769566105667cd478", size = 41472, upload-time = "2025-08-10T10:53:47.561Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/47/e43f2d8b88477a9b27b39d97e3c61534ae3cda4c99771f8b3c81d2469486/pygrister-0.8.0-py3-none-any.whl", hash = "sha256:b882a93db0aae642435d23f8e6f6a50f737befa35e3cce72f234bedd0ef4bee6", size = 31863, upload-time = "2025-08-10T10:53:46.345Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
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/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.1"
|
||||
@@ -468,6 +604,34 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reportlab"
|
||||
version = "4.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/fa/ed71f3e750afb77497641eb0194aeda069e271ce6d6931140f8787e0e69a/reportlab-4.4.4.tar.gz", hash = "sha256:cb2f658b7f4a15be2cc68f7203aa67faef67213edd4f2d4bdd3eb20dab75a80d", size = 3711935, upload-time = "2025-09-19T10:43:36.502Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/57/66/e040586fe6f9ae7f3a6986186653791fb865947f0b745290ee4ab026b834/reportlab-4.4.4-py3-none-any.whl", hash = "sha256:299b3b0534e7202bb94ed2ddcd7179b818dcda7de9d8518a57c85a58a1ebaadb", size = 1954981, upload-time = "2025-09-19T10:43:33.589Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.1.0"
|
||||
@@ -523,8 +687,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/3a/7e7ea6f0d31d3f5beb0f2cf2c4c362672f5f7f125714458673fc579e2bed/rignore-0.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:91dc94b1cc5af8d6d25ce6edd29e7351830f19b0a03b75cb3adf1f76d00f3007", size = 1134598, upload-time = "2025-07-19T19:24:15.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/06/1b3307f6437d29bede5a95738aa89e6d910ba68d4054175c9f60d8e2c6b1/rignore-0.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4d1918221a249e5342b60fd5fa513bf3d6bf272a8738e66023799f0c82ecd788", size = 1108862, upload-time = "2025-07-19T19:24:26.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/d5/b37c82519f335f2c472a63fc6215c6f4c51063ecf3166e3acf508011afbd/rignore-0.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:240777332b859dc89dcba59ab6e3f1e062bc8e862ffa3e5f456e93f7fd5cb415", size = 1120002, upload-time = "2025-07-19T19:24:38.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/72/2f05559ed5e69bdfdb56ea3982b48e6c0017c59f7241f7e1c5cae992b347/rignore-0.6.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b0e548753e55cc648f1e7b02d9f74285fe48bb49cec93643d31e563773ab3f", size = 949454, upload-time = "2025-07-19T19:23:38.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/92/186693c8f838d670510ac1dfb35afbe964320fbffb343ba18f3d24441941/rignore-0.6.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6971ac9fdd5a0bd299a181096f091c4f3fd286643adceba98eccc03c688a6637", size = 974663, upload-time = "2025-07-19T19:23:28.24Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -560,36 +722,23 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.43"
|
||||
version = "2.0.44"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlmodel"
|
||||
version = "0.0.24"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload-time = "2025-03-07T05:43:32.887Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload-time = "2025-03-07T05:43:30.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -719,26 +868,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114, upload-time = "2025-06-15T19:06:06.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879, upload-time = "2025-06-15T19:06:07.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026, upload-time = "2025-06-15T19:06:08.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917, upload-time = "2025-06-15T19:06:09.988Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602, upload-time = "2025-06-15T19:06:11.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758, upload-time = "2025-06-15T19:06:12.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601, upload-time = "2025-06-15T19:06:13.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936, upload-time = "2025-06-15T19:06:14.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243, upload-time = "2025-06-15T19:06:16.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073, upload-time = "2025-06-15T19:06:17.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872, upload-time = "2025-06-15T19:06:18.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877, upload-time = "2025-06-15T19:06:19.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645, upload-time = "2025-06-15T19:06:20.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424, upload-time = "2025-06-15T19:06:21.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584, upload-time = "2025-06-15T19:06:22.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675, upload-time = "2025-06-15T19:06:24.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363, upload-time = "2025-06-15T19:06:25.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
Reference in New Issue
Block a user