Compare commits
25 Commits
new-regist
...
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 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,8 @@
|
|||||||
config.php
|
config.php
|
||||||
test.php
|
test.php
|
||||||
melly-to-grist/.env
|
.env
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
new-registration-app/database.db
|
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
|
||||||
|
|
||||||
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,12 +1,15 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "meal-manager"
|
name = "meal-manager"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = "~=3.13.0"
|
requires-python = "~=3.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alembic>=1.17.0",
|
"alembic>=1.17.0",
|
||||||
"fastapi[standard]>=0.116.0",
|
"fastapi[standard]>=0.116.0",
|
||||||
|
"pygrister>=0.8.0",
|
||||||
|
"python-dotenv>=1.1.1",
|
||||||
|
"reportlab>=4.4.4",
|
||||||
"sqlalchemy>=2.0.44",
|
"sqlalchemy>=2.0.44",
|
||||||
"uvicorn[standard]>=0.35.0",
|
"uvicorn[standard]>=0.35.0",
|
||||||
]
|
]
|
||||||
@@ -18,7 +21,13 @@ build-backend = "uv_build"
|
|||||||
dev = [
|
dev = [
|
||||||
"black>=25.1.0",
|
"black>=25.1.0",
|
||||||
"isort>=6.0.1",
|
"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]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|
||||||
@@ -28,7 +37,7 @@ profile = "black"
|
|||||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
# 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
|
# format, relative to the token %(here)s which refers to the location of this
|
||||||
# ini file
|
# ini file
|
||||||
script_location = "%(here)s/alembic"
|
script_location = "%(here)s/src/meal_manager/alembic"
|
||||||
|
|
||||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
# 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
|
# Uncomment the line below if you want the files to be prepended with date and time
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ def upgrade() -> None:
|
|||||||
sa.PrimaryKeyConstraint("household_id"),
|
sa.PrimaryKeyConstraint("household_id"),
|
||||||
)
|
)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"team_registration",
|
"teamregistration",
|
||||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column("event_id", sa.Integer(), nullable=False),
|
sa.Column("event_id", sa.Integer(), nullable=False),
|
||||||
sa.Column("person_name", sa.String(), nullable=False),
|
sa.Column("person_name", sa.String(), nullable=False),
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
import locale
|
import locale
|
||||||
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from functools import partial
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
import starlette.status as status
|
import starlette.status as status
|
||||||
from fastapi import Depends, FastAPI, Request
|
from fastapi import Depends, FastAPI, HTTPException, Request, Response
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import FileResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from sqlalchemy import create_engine, select
|
from sqlalchemy import create_engine, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from meal_manager.grist import sync_with_grist
|
||||||
from meal_manager.models import (
|
from meal_manager.models import (
|
||||||
Base,
|
Base,
|
||||||
Event,
|
Event,
|
||||||
@@ -19,6 +23,7 @@ from meal_manager.models import (
|
|||||||
Subscription,
|
Subscription,
|
||||||
TeamRegistration,
|
TeamRegistration,
|
||||||
)
|
)
|
||||||
|
from meal_manager.pdf import build_dinner_overview_pdf
|
||||||
|
|
||||||
sqlite_file_name = "database.db"
|
sqlite_file_name = "database.db"
|
||||||
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
||||||
@@ -34,6 +39,39 @@ def get_session():
|
|||||||
yield 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():
|
def create_db_and_tables():
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
@@ -51,9 +89,12 @@ templates = Jinja2Templates(directory="src/meal_manager/templates")
|
|||||||
|
|
||||||
SessionDep = Annotated[Session, Depends(get_session)]
|
SessionDep = Annotated[Session, Depends(get_session)]
|
||||||
|
|
||||||
|
UserDep = Annotated[dict, Depends(get_user)]
|
||||||
|
StrictUserDep = Annotated[dict, Depends(partial(get_user, allow_none=False))]
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def index(request: Request, session: SessionDep):
|
async def index(request: Request, session: SessionDep, user: UserDep):
|
||||||
"""Displays coming events and a button to register new ones"""
|
"""Displays coming events and a button to register new ones"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
# TODO: Once we refactored to use SQLAlchemy directly, we can probably do a nicer filtering on the date alone
|
# TODO: Once we refactored to use SQLAlchemy directly, we can probably do a nicer filtering on the date alone
|
||||||
@@ -66,10 +107,15 @@ async def index(request: Request, session: SessionDep):
|
|||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="index.html",
|
name="index.html",
|
||||||
context={"events": events, "current_page": "home", "now": now},
|
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")
|
@app.get("/past_events")
|
||||||
async def past_events(request: Request, session: SessionDep):
|
async def past_events(request: Request, session: SessionDep):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
@@ -88,11 +134,11 @@ async def past_events(request: Request, session: SessionDep):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/subscribe")
|
@app.get("/subscribe")
|
||||||
async def subscribe(request: Request, session: SessionDep):
|
async def subscribe(request: Request, session: SessionDep, user: UserDep):
|
||||||
statement = select(Household)
|
statement = select(Household)
|
||||||
households = session.scalars(statement)
|
households = session.scalars(statement)
|
||||||
|
|
||||||
subscriptions = session.scalars(select(Subscription))
|
subscriptions = session.scalars(select(Subscription)).all()
|
||||||
|
|
||||||
# filter out households with existing subscriptions
|
# filter out households with existing subscriptions
|
||||||
households = [
|
households = [
|
||||||
@@ -102,7 +148,11 @@ async def subscribe(request: Request, session: SessionDep):
|
|||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="subscribe.html",
|
name="subscribe.html",
|
||||||
context={"households": households, "subscriptions": subscriptions},
|
context={
|
||||||
|
"households": households,
|
||||||
|
"subscriptions": subscriptions,
|
||||||
|
"user": user,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -143,7 +193,9 @@ async def add_subscribe(request: Request, session: SessionDep):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/subscribe/{household_id}/delete")
|
@app.get("/subscribe/{household_id}/delete")
|
||||||
async def delete_subscription(request: Request, session: SessionDep, household_id: int):
|
async def delete_subscription(
|
||||||
|
request: Request, session: SessionDep, household_id: int, user: StrictUserDep
|
||||||
|
):
|
||||||
|
|
||||||
statement = select(Subscription).where(Subscription.household_id == household_id)
|
statement = select(Subscription).where(Subscription.household_id == household_id)
|
||||||
sub = session.scalars(statement).one()
|
sub = session.scalars(statement).one()
|
||||||
@@ -155,14 +207,70 @@ async def delete_subscription(request: Request, session: SessionDep, household_i
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/event/add")
|
@app.get("/event/add")
|
||||||
async def add_event_form(request: Request, session: SessionDep):
|
async def add_event_form(request: Request, user: StrictUserDep):
|
||||||
return templates.TemplateResponse(request=request, name="add_event.html")
|
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")
|
@app.post("/event/add")
|
||||||
async def add_event(request: Request, session: SessionDep):
|
async def add_event(request: Request, session: SessionDep, user: StrictUserDep):
|
||||||
form_data = await request.form()
|
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"])
|
event_time = datetime.fromisoformat(form_data["eventTime"])
|
||||||
registration_deadline = form_data.get("registrationDeadline")
|
registration_deadline = form_data.get("registrationDeadline")
|
||||||
if not registration_deadline:
|
if not registration_deadline:
|
||||||
@@ -175,21 +283,32 @@ async def add_event(request: Request, session: SessionDep):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
registration_deadline = datetime.fromisoformat(registration_deadline)
|
registration_deadline = datetime.fromisoformat(registration_deadline)
|
||||||
|
return event_time, registration_deadline
|
||||||
|
|
||||||
event = Event(
|
|
||||||
title=form_data["eventName"],
|
@app.get("/event/{event_id}/delete")
|
||||||
event_time=event_time,
|
async def delete_event(
|
||||||
registration_deadline=registration_deadline,
|
request: Request, session: SessionDep, event_id: int, user: StrictUserDep
|
||||||
description=form_data.get("eventDescription"),
|
):
|
||||||
recipe_link=form_data.get("recipeLink"),
|
if not user["admin"]:
|
||||||
)
|
raise HTTPException(status_code=403, detail="Not authorized")
|
||||||
session.add(event)
|
|
||||||
|
statement = select(Event).where(Event.id == event_id)
|
||||||
|
event = session.scalars(statement).one()
|
||||||
|
|
||||||
|
session.delete(event)
|
||||||
session.commit()
|
session.commit()
|
||||||
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/event/{event_id}")
|
@app.get("/event/{event_id}")
|
||||||
async def read_event(request: Request, event_id: int, session: SessionDep):
|
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)
|
statement = select(Event).where(Event.id == event_id)
|
||||||
event = session.scalars(statement).one()
|
event = session.scalars(statement).one()
|
||||||
|
|
||||||
@@ -206,7 +325,13 @@ async def read_event(request: Request, event_id: int, session: SessionDep):
|
|||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="event.html",
|
name="event.html",
|
||||||
context={"event": event, "households": households, "now": datetime.now()},
|
context={
|
||||||
|
"event": event,
|
||||||
|
"households": households,
|
||||||
|
"now": datetime.now(),
|
||||||
|
"user": user,
|
||||||
|
"message": message,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -239,7 +364,11 @@ async def add_registration(request: Request, event_id: int, session: SessionDep)
|
|||||||
|
|
||||||
@app.get("/event/{event_id}/registration/{household_id}/delete")
|
@app.get("/event/{event_id}/registration/{household_id}/delete")
|
||||||
async def delete_registration(
|
async def delete_registration(
|
||||||
request: Request, event_id: int, household_id: int, session: SessionDep
|
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
|
Deletes a registration record for a specific household at a given event. This endpoint
|
||||||
@@ -262,7 +391,9 @@ async def add_team_registration(request: Request, event_id: int, session: Sessio
|
|||||||
work_type = form_data["workType"]
|
work_type = form_data["workType"]
|
||||||
|
|
||||||
statement = select(TeamRegistration).where(
|
statement = select(TeamRegistration).where(
|
||||||
TeamRegistration.person_name == person, TeamRegistration.work_type == work_type
|
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 the person has already registered for the same work type, just ignore
|
||||||
if session.scalars(statement).one_or_none() is None:
|
if session.scalars(statement).one_or_none() is None:
|
||||||
@@ -282,8 +413,52 @@ async def delete_team_registration(
|
|||||||
event_id: int,
|
event_id: int,
|
||||||
entry_id: int,
|
entry_id: int,
|
||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
|
user: StrictUserDep,
|
||||||
):
|
):
|
||||||
statement = select(TeamRegistration).where(TeamRegistration.id == entry_id)
|
statement = select(TeamRegistration).where(TeamRegistration.id == entry_id)
|
||||||
session.delete(session.scalars(statement).one())
|
session.delete(session.scalars(statement).one())
|
||||||
session.commit()
|
session.commit()
|
||||||
return RedirectResponse(url=f"/event/{event_id}", status_code=status.HTTP_302_FOUND)
|
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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import typing
|
import typing
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey
|
from sqlalchemy import ForeignKey
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
@@ -19,8 +19,8 @@ class Event(Base):
|
|||||||
title: Mapped[str] = mapped_column(nullable=False)
|
title: Mapped[str] = mapped_column(nullable=False)
|
||||||
event_time: Mapped[datetime] = mapped_column(nullable=False)
|
event_time: Mapped[datetime] = mapped_column(nullable=False)
|
||||||
registration_deadline: Mapped[datetime] = mapped_column(nullable=False)
|
registration_deadline: Mapped[datetime] = mapped_column(nullable=False)
|
||||||
description: Mapped[str] = mapped_column()
|
description: Mapped[str] = mapped_column(nullable=True)
|
||||||
recipe_link: Mapped[str] = mapped_column()
|
recipe_link: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
||||||
# Min and max number of people needed for cooking, doing the dishes and preparing the tables
|
# 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_min: Mapped[int] = mapped_column(default=3, nullable=False)
|
||||||
@@ -33,8 +33,20 @@ class Event(Base):
|
|||||||
team_prep_min: Mapped[int] = mapped_column(default=1, nullable=False)
|
team_prep_min: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||||
team_prep_max: Mapped[int] = mapped_column(default=1, nullable=False)
|
team_prep_max: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||||
|
|
||||||
registrations: Mapped[list["Registration"]] = relationship("Registration")
|
subscriptions_applied: Mapped[bool] = mapped_column(default=False, nullable=False)
|
||||||
team: Mapped[list["TeamRegistration"]] = relationship("TeamRegistration")
|
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):
|
def team_min_reached(self, work_type: WorkTypes):
|
||||||
threshold = {
|
threshold = {
|
||||||
@@ -62,9 +74,17 @@ class Event(Base):
|
|||||||
self.team_max_reached(work_type) for work_type in typing.get_args(WorkTypes)
|
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):
|
class TeamRegistration(Base):
|
||||||
__tablename__ = "team_registration"
|
__tablename__ = "teamregistration"
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
event_id: Mapped[int] = mapped_column(ForeignKey("event.id"))
|
event_id: Mapped[int] = mapped_column(ForeignKey("event.id"))
|
||||||
person_name: Mapped[str] = mapped_column(nullable=False)
|
person_name: Mapped[str] = mapped_column(nullable=False)
|
||||||
@@ -141,3 +161,15 @@ class Subscription(Base):
|
|||||||
return ", ".join(result)
|
return ", ".join(result)
|
||||||
else:
|
else:
|
||||||
return "Alle"
|
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)
|
||||||
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: /
|
||||||
@@ -3,35 +3,57 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center mt-4">
|
<div class="row justify-content-center mt-4">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<h2>Neues Event erstellen</h2>
|
{% if edit_mode %}<h2>Event bearbeiten</h2>{% else %}<h2>Neues Event erstellen</h2>{% endif %}
|
||||||
<form method="post" action="/event/add">
|
{% if edit_mode %}<form method="post" action="/event/{{ event.id }}/edit">{% else %}<form method="post" action="/event/add">{% endif %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="eventName" class="form-label">Event Name</label>
|
<label for="eventName" class="form-label">Event Name</label>
|
||||||
<input type="text" class="form-control" id="eventName" name="eventName" required>
|
<input type="text" class="form-control" id="eventName" name="eventName" required {% if edit_mode %}value="{{ event.title }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="eventTime" class="form-label">Datum und Uhrzeit</label>
|
<label for="eventTime" class="form-label">Datum und Uhrzeit</label>
|
||||||
<input type="datetime-local" class="form-control" id="eventTime" name="eventTime" required>
|
<input type="datetime-local" class="form-control" id="eventTime" name="eventTime" required {% if edit_mode %}value="{{ event.event_time }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="registrationDeadline" class="form-label">Anmeldungs-Deadline</label>
|
<label for="registrationDeadline" class="form-label">Anmeldungs-Deadline</label>
|
||||||
<input type="datetime-local" class="form-control" id="registrationDeadline" name="registrationDeadline">
|
<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>
|
<small class="form-text text-muted">Leer lassen für Sonntag Abend vor dem Event</small>
|
||||||
</div>
|
</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">
|
<div class="mb-3">
|
||||||
<label for="recipeLink" class="form-label">Rezept-Link</label>
|
<label for="recipeLink" class="form-label">Rezept-Link</label>
|
||||||
<input type="text" class="form-control" id="recipeLink" name="recipeLink">
|
<input type="text" class="form-control" id="recipeLink" name="recipeLink" {% if edit_mode %}value="{{ event.recipe_link }}"{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="eventDescription" class="form-label">Beschreibung</label>
|
<label for="eventDescription" class="form-label">Beschreibung</label>
|
||||||
<textarea class="form-control" id="eventDescription" name="eventDescription" rows="3"></textarea>
|
<textarea class="form-control" id="eventDescription" name="eventDescription" rows="3" {% if edit_mode %}value="{{ event.description }}"{% endif %}></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Event erstellen</button>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
{% import "macros.j2" as macros %}
|
||||||
{% macro teamEntries(event, work_type) -%}
|
{% macro teamEntries(event, work_type) -%}
|
||||||
{% for entry in event.team | selectattr("work_type", "equalto", 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">
|
<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>
|
<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">
|
<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">
|
<a href="/event/{{event.id}}/register_team/{{entry.id}}/delete">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif -%}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
@@ -15,24 +18,73 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<p class="h1">{{ event.title }}</p>
|
<p class="h1">{{ event.title }}</p>
|
||||||
<p class="text-muted">{{ event.event_time.strftime('%A, %d.%m.%Y') }}</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>
|
<p>{{ event.description }}</p>
|
||||||
<hr class="hr"/>
|
<hr class="hr"/>
|
||||||
|
{% 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">
|
<div class="container">
|
||||||
|
|
||||||
<p class="h3">Anmeldungen</p>
|
<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="row m-2">
|
||||||
<div class="col-md-4 d-flex justify-content-center align-items-center flex-column">
|
<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>
|
<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 trigger modal -->
|
||||||
<button type="button" class="btn btn-primary mb-2 w-100" {% if event.registration_deadline < now %}disabled{% endif%} 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
|
Anmeldung hinzufügen
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-primary mb-2 w-100" {% if event.all_teams_max() or event.event_time < now %}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
|
Dienst übernehmen
|
||||||
</button>
|
</button>
|
||||||
{% if event.recipe_link %}
|
{% if event.recipe_link %}
|
||||||
<a href="{{ event.recipe_link }}" class="btn btn-outline-primary mb-2 w-100" target="_blank">
|
<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
|
<i class="bi bi-book"></i> Original Rezept ansehen
|
||||||
</a>
|
</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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@@ -105,7 +157,7 @@
|
|||||||
<th scope="col">Kinder</th>
|
<th scope="col">Kinder</th>
|
||||||
<th scope="col">Kleinkinder</th>
|
<th scope="col">Kleinkinder</th>
|
||||||
<th scope="col">Kommentar</th>
|
<th scope="col">Kommentar</th>
|
||||||
<th scope="col">Löschen</th>
|
{% if user %}<th scope="col">Löschen</th>{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -115,8 +167,8 @@
|
|||||||
<td>{{ reg.num_adult_meals }}</td>
|
<td>{{ reg.num_adult_meals }}</td>
|
||||||
<td>{{ reg.num_children_meals }}</td>
|
<td>{{ reg.num_children_meals }}</td>
|
||||||
<td>{{ reg.num_small_children_meals }}</td>
|
<td>{{ reg.num_small_children_meals }}</td>
|
||||||
<td>{{ reg.comment }}</td>
|
<td>{% if reg.comment %}{{ reg.comment }}{% endif %}</td>
|
||||||
<td><a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete"><i class="bi bi-trash"></i></a></td>
|
{% if user %}<td><a href="/event/{{event.id}}/registration/{{reg.household_id}}/delete"><i class="bi bi-trash"></i></a></td>{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -130,9 +182,9 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
<h5 class="card-title mb-0">{{ reg.household.name }}</h5>
|
<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>
|
<i class="bi bi-trash"></i>
|
||||||
</a>
|
</a>{% endif -%}
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-3 text-center">
|
<div class="col-3 text-center">
|
||||||
@@ -254,4 +306,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "macros.j2" as macros %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mt-4 mb-3">
|
<div class="row mt-4 mb-3">
|
||||||
<div class="col d-flex justify-content-between align-items-center">
|
<div class="col d-flex justify-content-between align-items-center">
|
||||||
<h2>{% if current_page == "home" %}Kommende{% else %}Vergangene{% endif %} Kochabende</h2>
|
<h2>{% if current_page == "home" %}Kommende{% else %}Vergangene{% endif %} Kochabende</h2>
|
||||||
|
{% if user %}
|
||||||
<a href="/event/add" class="btn btn-primary">
|
<a href="/event/add" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Neues Event erstellen
|
<i class="bi bi-plus-circle"></i> Neues Event erstellen
|
||||||
</a>
|
</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>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@@ -26,18 +33,28 @@
|
|||||||
|
|
||||||
|
|
||||||
{% for event in events %}
|
{% for event in events %}
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card h-100 shadow-sm">
|
<div class="card h-100 shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title mb-1">{{ event.title }}</h5>
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
<p class="text-muted mb-3"><i class="bi bi-calendar"></i> {{ event.event_time.strftime('%A, %d.%m.%Y')
|
<div>
|
||||||
}}
|
<h5 class="card-title mb-0">
|
||||||
</p>
|
{{ event.title }}
|
||||||
<p class="card-text">{{ event.description }}</p>
|
{% if event.billed %}<i class="bi bi-cash-coin" title="Abgerechnet"></i>{% endif %}
|
||||||
<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>
|
{% if event.exclude_from_billing %}<i class="bi bi-ban" title="Keine Abrechung"></i>{% endif %}
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
{{ macros.login_info_modal() }}
|
||||||
{% endblock %}
|
{% 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 %}
|
||||||
@@ -4,6 +4,12 @@
|
|||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<!-- Left column: subscription form -->
|
<!-- Left column: subscription form -->
|
||||||
<div class="col-12 col-lg-6">
|
<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="row justify-content-center">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm mt-4">
|
<div class="card shadow-sm mt-4">
|
||||||
@@ -106,9 +112,9 @@
|
|||||||
<div class="card-body py-2 px-3">
|
<div class="card-body py-2 px-3">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
<h6 class="mb-0">{{ sub.household.name }}</h6>
|
<h6 class="mb-0">{{ sub.household.name }}</h6>
|
||||||
<a href="/subscribe/{{sub.household.id}}/delete" class="text-danger">
|
{% if user %}<a href="/subscribe/{{sub.household.id}}/delete" class="text-danger">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</a>
|
</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-3 text-center">
|
<div class="col-3 text-center">
|
||||||
|
|||||||
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
|
||||||
143
uv.lock
generated
143
uv.lock
generated
@@ -67,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.2.1"
|
version = "8.2.1"
|
||||||
@@ -250,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "isort"
|
name = "isort"
|
||||||
version = "6.0.1"
|
version = "6.0.1"
|
||||||
@@ -334,11 +368,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "meal-manager"
|
name = "meal-manager"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
|
{ name = "pygrister" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "reportlab" },
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
@@ -347,12 +384,16 @@ dependencies = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "black" },
|
{ name = "black" },
|
||||||
{ name = "isort" },
|
{ name = "isort" },
|
||||||
|
{ name = "pytest" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "alembic", specifier = ">=1.17.0" },
|
{ name = "alembic", specifier = ">=1.17.0" },
|
||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.116.0" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.116.0" },
|
||||||
|
{ 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 = "sqlalchemy", specifier = ">=2.0.44" },
|
||||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" },
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" },
|
||||||
]
|
]
|
||||||
@@ -361,6 +402,7 @@ requires-dist = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "black", specifier = ">=25.1.0" },
|
{ name = "black", specifier = ">=25.1.0" },
|
||||||
{ name = "isort", specifier = ">=6.0.1" },
|
{ name = "isort", specifier = ">=6.0.1" },
|
||||||
|
{ name = "pytest", specifier = ">=8.4.2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -390,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.4.0"
|
version = "4.4.0"
|
||||||
@@ -399,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.11.7"
|
version = "2.11.7"
|
||||||
@@ -456,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -491,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "14.1.0"
|
version = "14.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user