Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91f924a75a | |||
| e1605aab29 | |||
| 1164d492a3 |
3
.envrc
3
.envrc
@@ -1 +1,2 @@
|
||||
use flake
|
||||
# devenv needs to know the path to the current working directory to create and manage mutable state
|
||||
use flake . --no-pure-eval
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,11 +1,12 @@
|
||||
result
|
||||
result-*
|
||||
**/__pycache__
|
||||
dibbler.egg-info
|
||||
.venv
|
||||
.direnv
|
||||
.devenv
|
||||
|
||||
dist
|
||||
|
||||
config.ini
|
||||
test.db
|
||||
|
||||
.ruff_cache
|
||||
|
||||
*.qcow2
|
||||
|
||||
76
README.md
76
README.md
@@ -2,55 +2,47 @@
|
||||
|
||||
EDB-system for PVVVV
|
||||
|
||||
## Hva er dette?
|
||||
|
||||
Dibbler er et system laget av PVVere for PVVere for å byttelåne både matvarer og godis.
|
||||
Det er designet for en gammeldags VT terminal, og er laget for å være enkelt både å bruke og å hacke på.
|
||||
|
||||
Programmet er skrevet i Python, og bruker en sql database for å lagre data.
|
||||
|
||||
Samlespleiseboden er satt opp slik at folk kjøper inn varer, og får dibblerkreditt, og så kan man bruke
|
||||
denne kreditten til å kjøpe ut andre varer. Det er ikke noen form for authentisering, så hele systemet er basert på tillit.
|
||||
Det er anbefalt å koble en barkodeleser til systemet for å gjøre det enklere å både legge til og kjøpe varer.
|
||||
|
||||
## Kom i gang
|
||||
|
||||
Installer python, og lag og aktiver et venv. Installer så avhengighetene med `pip install`.
|
||||
|
||||
Deretter kan du kjøre programmet med
|
||||
|
||||
```console
|
||||
python -m dibbler -c example-config.toml create-db
|
||||
python -m dibbler -c example-config.toml seed-data
|
||||
python -m dibbler -c example-config.toml loop
|
||||
```
|
||||
|
||||
## Nix
|
||||
|
||||
> [!NOTE]
|
||||
> Vi har skrevet nix-kode for å generere en QEMU-VM med tilnærmet produksjonsoppsett.
|
||||
> Det kjører ikke nødvendigvis noen VM-er i produksjon, og ihvertfall ikke denne VM-en.
|
||||
> Den er hovedsakelig laget for enkel interaktiv testing, og for å teste NixOS modulen.
|
||||
### Hvordan kjøre
|
||||
|
||||
Du kan enklest komme i gang med nix-utvikling ved å kjøre test VM-en:
|
||||
nix run github:Programvareverkstedet/dibbler
|
||||
|
||||
```console
|
||||
nix run .#vm
|
||||
### Hvordan utvikle?
|
||||
|
||||
# Eller hvis du trenger tilgang til terminalen i VM-en også:
|
||||
nix run .#vm-non-kiosk
|
||||
```
|
||||
python -m venv .venv
|
||||
source .venv/activate
|
||||
pip install -e .
|
||||
cp example-config.ini config.ini
|
||||
dibbler -c config.ini create-db
|
||||
dibbler -c config.ini loop
|
||||
|
||||
Du kan også bygge pakken manuelt, eller kjøre den direkte:
|
||||
eller hvis du tolererer nix og postgres:
|
||||
|
||||
```console
|
||||
nix build .#dibbler
|
||||
direnv allow # eller bare `nix develop`
|
||||
devenv up
|
||||
dibbler create-db
|
||||
dibbler loop
|
||||
|
||||
nix run .# -- --config example-config.toml create-db
|
||||
nix run .# -- --config example-config.toml seed-data
|
||||
nix run .# -- --config example-config.toml loop
|
||||
```
|
||||
### Bygge image
|
||||
|
||||
## Produksjonssetting
|
||||
For å bygge et image trenger du en builder som takler å bygge for arkitekturen du skal lage et image for.
|
||||
|
||||
Se https://wiki.pvv.ntnu.no/wiki/Drift/Dibbler
|
||||
_(Eller be til gudene om at cross compile funker)_
|
||||
|
||||
Flaket exposer en modul som autologger inn med en bruker som automatisk kjører dibbler, og setter opp et minimalistisk miljø.
|
||||
|
||||
Før du bygger imaget burde du lage en `config.ini` fil lokalt som inneholder instillingene dine. **NB: Denne kommer til å ligge i nix storen.**
|
||||
|
||||
Du kan også endre hvilken `config.ini` som blir brukt direkte i pakken eller i modulen.
|
||||
|
||||
Se eksempelet for hvordan skrot er satt opp i `flake.nix`
|
||||
|
||||
### Bygge image for skrot
|
||||
|
||||
Skrot har et system image definert i `flake.nix`:
|
||||
|
||||
1. lag `config.ini` (`cp {example-,}config.ini`)
|
||||
2. `nix build .#images.skrot`
|
||||
3. ???
|
||||
4. non-profit!
|
||||
|
||||
4
default.nix
Normal file
4
default.nix
Normal file
@@ -0,0 +1,4 @@
|
||||
{ pkgs ? import <nixos-unstable> { } }:
|
||||
{
|
||||
dibbler = pkgs.callPackage ./nix/dibbler.nix { };
|
||||
}
|
||||
@@ -1,72 +1,6 @@
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
import os
|
||||
import sys
|
||||
# This module is supposed to act as a singleton and be filled
|
||||
# with config variables by cli.py
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path("/etc/dibbler/dibbler.toml")
|
||||
import configparser
|
||||
|
||||
|
||||
def default_config_path_submissive_and_readable() -> bool:
|
||||
return DEFAULT_CONFIG_PATH.is_file() and any(
|
||||
[
|
||||
(
|
||||
DEFAULT_CONFIG_PATH.stat().st_mode & 0o400
|
||||
and DEFAULT_CONFIG_PATH.stat().st_uid == os.getuid()
|
||||
),
|
||||
(
|
||||
DEFAULT_CONFIG_PATH.stat().st_mode & 0o040
|
||||
and DEFAULT_CONFIG_PATH.stat().st_gid == os.getgid()
|
||||
),
|
||||
(DEFAULT_CONFIG_PATH.stat().st_mode & 0o004),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
config: dict[str, dict[str, Any]] = dict()
|
||||
|
||||
|
||||
def load_config(config_path: Path | None = None):
|
||||
global config
|
||||
if config_path is not None:
|
||||
with Path(config_path).open("rb") as file:
|
||||
config = tomllib.load(file)
|
||||
elif default_config_path_submissive_and_readable():
|
||||
with DEFAULT_CONFIG_PATH.open("rb") as file:
|
||||
config = tomllib.load(file)
|
||||
else:
|
||||
print(
|
||||
"Could not read config file, it was neither provided nor readable in default location",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def config_db_string() -> str:
|
||||
db_type = config["database"]["type"]
|
||||
|
||||
if db_type == "sqlite":
|
||||
path = Path(config["database"]["sqlite"]["path"])
|
||||
return f"sqlite:///{path.absolute()}"
|
||||
|
||||
elif db_type == "postgresql":
|
||||
host = config["database"]["postgresql"]["host"]
|
||||
port = config["database"]["postgresql"].get("port", 5432)
|
||||
username = config["database"]["postgresql"].get("username", "dibbler")
|
||||
dbname = config["database"]["postgresql"].get("dbname", "dibbler")
|
||||
|
||||
if "password_file" in config["database"]["postgresql"]:
|
||||
with Path(config["database"]["postgresql"]["password_file"]).open("r") as f:
|
||||
password = f.read().strip()
|
||||
elif "password" in config["database"]["postgresql"]:
|
||||
password = config["database"]["postgresql"]["password"]
|
||||
else:
|
||||
password = ""
|
||||
|
||||
if host.startswith("/"):
|
||||
return f"postgresql+psycopg2://{username}:{password}@/{dbname}?host={host}"
|
||||
else:
|
||||
return f"postgresql+psycopg2://{username}:{password}@{host}:{port}/{dbname}"
|
||||
else:
|
||||
print(f"Error: unknown database type '{db_type}'")
|
||||
exit(1)
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
12
dibbler/db.py
Normal file
12
dibbler/db.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import os
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from dibbler.conf import config
|
||||
|
||||
engine = create_engine(
|
||||
os.environ.get("DIBBLER_DATABASE_URL")
|
||||
or config.get("database", "url")
|
||||
)
|
||||
Session = sessionmaker(bind=engine)
|
||||
@@ -1,71 +1,70 @@
|
||||
# import os
|
||||
import os
|
||||
|
||||
# from PIL import ImageFont
|
||||
# from barcode.writer import ImageWriter, mm2px
|
||||
# from brother_ql.labels import ALL_LABELS
|
||||
from PIL import ImageFont
|
||||
from barcode.writer import ImageWriter, mm2px
|
||||
from brother_ql.devicedependent import label_type_specs
|
||||
|
||||
|
||||
# def px2mm(px, dpi=300):
|
||||
# return (25.4 * px) / dpi
|
||||
def px2mm(px, dpi=300):
|
||||
return (25.4 * px) / dpi
|
||||
|
||||
|
||||
# class BrotherLabelWriter(ImageWriter):
|
||||
# def __init__(self, typ="62", max_height=350, rot=False, text=None):
|
||||
# super(BrotherLabelWriter, self).__init__()
|
||||
# label = next([l for l in ALL_LABELS if l.identifier == typ])
|
||||
# assert label is not None
|
||||
# self.rot = rot
|
||||
# if self.rot:
|
||||
# self._h, self._w = label.dots_printable
|
||||
# if self._w == 0 or self._w > max_height:
|
||||
# self._w = min(max_height, self._h / 2)
|
||||
# else:
|
||||
# self._w, self._h = label.dots_printable
|
||||
# if self._h == 0 or self._h > max_height:
|
||||
# self._h = min(max_height, self._w / 2)
|
||||
# self._xo = 0.0
|
||||
# self._yo = 0.0
|
||||
# self._title = text
|
||||
class BrotherLabelWriter(ImageWriter):
|
||||
def __init__(self, typ="62", max_height=350, rot=False, text=None):
|
||||
super(BrotherLabelWriter, self).__init__()
|
||||
assert typ in label_type_specs
|
||||
self.rot = rot
|
||||
if self.rot:
|
||||
self._h, self._w = label_type_specs[typ]["dots_printable"]
|
||||
if self._w == 0 or self._w > max_height:
|
||||
self._w = min(max_height, self._h / 2)
|
||||
else:
|
||||
self._w, self._h = label_type_specs[typ]["dots_printable"]
|
||||
if self._h == 0 or self._h > max_height:
|
||||
self._h = min(max_height, self._w / 2)
|
||||
self._xo = 0.0
|
||||
self._yo = 0.0
|
||||
self._title = text
|
||||
|
||||
# def _init(self, code):
|
||||
# self.text = None
|
||||
# super(BrotherLabelWriter, self)._init(code)
|
||||
def _init(self, code):
|
||||
self.text = None
|
||||
super(BrotherLabelWriter, self)._init(code)
|
||||
|
||||
# def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
|
||||
# x, y = super(BrotherLabelWriter, self).calculate_size(
|
||||
# modules_per_line, number_of_lines, dpi
|
||||
# )
|
||||
def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
|
||||
x, y = super(BrotherLabelWriter, self).calculate_size(
|
||||
modules_per_line, number_of_lines, dpi
|
||||
)
|
||||
|
||||
# self._xo = (px2mm(self._w) - px2mm(x)) / 2
|
||||
# self._yo = px2mm(self._h) - px2mm(y)
|
||||
# assert self._xo >= 0
|
||||
# assert self._yo >= 0
|
||||
self._xo = (px2mm(self._w) - px2mm(x)) / 2
|
||||
self._yo = px2mm(self._h) - px2mm(y)
|
||||
assert self._xo >= 0
|
||||
assert self._yo >= 0
|
||||
|
||||
# return int(self._w), int(self._h)
|
||||
return int(self._w), int(self._h)
|
||||
|
||||
# def _paint_module(self, xpos, ypos, width, color):
|
||||
# super(BrotherLabelWriter, self)._paint_module(
|
||||
# xpos + self._xo, ypos + self._yo, width, color
|
||||
# )
|
||||
def _paint_module(self, xpos, ypos, width, color):
|
||||
super(BrotherLabelWriter, self)._paint_module(
|
||||
xpos + self._xo, ypos + self._yo, width, color
|
||||
)
|
||||
|
||||
# def _paint_text(self, xpos, ypos):
|
||||
# super(BrotherLabelWriter, self)._paint_text(xpos + self._xo, ypos + self._yo)
|
||||
def _paint_text(self, xpos, ypos):
|
||||
super(BrotherLabelWriter, self)._paint_text(xpos + self._xo, ypos + self._yo)
|
||||
|
||||
# def _finish(self):
|
||||
# if self._title:
|
||||
# width = self._w + 1
|
||||
# height = 0
|
||||
# max_h = self._h - mm2px(self._yo, self.dpi)
|
||||
# fs = int(max_h / 1.2)
|
||||
# font_path = os.path.join(
|
||||
# os.path.dirname(os.path.realpath(__file__)),
|
||||
# "Stranger back in the Night.ttf",
|
||||
# )
|
||||
# font = ImageFont.truetype(font_path, 10)
|
||||
# while width > self._w or height > max_h:
|
||||
# font = ImageFont.truetype(font_path, fs)
|
||||
# width, height = font.getsize(self._title)
|
||||
# fs -= 1
|
||||
# pos = ((self._w - width) // 2, 0 - (height // 8))
|
||||
# self._draw.text(pos, self._title, font=font, fill=self.foreground)
|
||||
# return self._image
|
||||
def _finish(self):
|
||||
if self._title:
|
||||
width = self._w + 1
|
||||
height = 0
|
||||
max_h = self._h - mm2px(self._yo, self.dpi)
|
||||
fs = int(max_h / 1.2)
|
||||
font_path = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"Stranger back in the Night.ttf",
|
||||
)
|
||||
font = ImageFont.truetype(font_path, 10)
|
||||
while width > self._w or height > max_h:
|
||||
font = ImageFont.truetype(font_path, fs)
|
||||
width, height = font.getsize(self._title)
|
||||
fs -= 1
|
||||
pos = ((self._w - width) // 2, 0 - (height // 8))
|
||||
self._draw.text(pos, self._title, font=font, fill=self.foreground)
|
||||
return self._image
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
import os
|
||||
import pwd
|
||||
import signal
|
||||
import subprocess
|
||||
from typing import Any, Callable, Literal
|
||||
import os
|
||||
import signal
|
||||
|
||||
from sqlalchemy import and_, not_, or_
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, and_
|
||||
|
||||
from ..models import Product, User
|
||||
from ..models import User, Product
|
||||
|
||||
|
||||
def search_user(
|
||||
string: str,
|
||||
sql_session: Session,
|
||||
ignorethisflag=None,
|
||||
):
|
||||
assert sql_session is not None
|
||||
def search_user(string, session, ignorethisflag=None):
|
||||
string = string.lower()
|
||||
exact_match = (
|
||||
sql_session.query(User)
|
||||
session.query(User)
|
||||
.filter(or_(User.name == string, User.card == string, User.rfid == string))
|
||||
.first()
|
||||
)
|
||||
if exact_match:
|
||||
return exact_match
|
||||
user_list = (
|
||||
sql_session.query(User)
|
||||
session.query(User)
|
||||
.filter(
|
||||
or_(
|
||||
User.name.ilike(f"%{string}%"),
|
||||
@@ -38,28 +31,20 @@ def search_user(
|
||||
return user_list
|
||||
|
||||
|
||||
def search_product(
|
||||
string: str,
|
||||
sql_session: Session,
|
||||
find_hidden_products: bool = True,
|
||||
):
|
||||
assert sql_session is not None
|
||||
def search_product(string, session, find_hidden_products=True):
|
||||
if find_hidden_products:
|
||||
exact_match = (
|
||||
sql_session.query(Product)
|
||||
session.query(Product)
|
||||
.filter(or_(Product.bar_code == string, Product.name == string))
|
||||
.first()
|
||||
)
|
||||
else:
|
||||
exact_match = (
|
||||
sql_session.query(Product)
|
||||
session.query(Product)
|
||||
.filter(
|
||||
or_(
|
||||
Product.bar_code == string,
|
||||
and_(
|
||||
Product.name == string,
|
||||
not_(Product.hidden),
|
||||
),
|
||||
and_(Product.name == string, Product.hidden is False),
|
||||
)
|
||||
)
|
||||
.first()
|
||||
@@ -68,7 +53,7 @@ def search_product(
|
||||
return exact_match
|
||||
if find_hidden_products:
|
||||
product_list = (
|
||||
sql_session.query(Product)
|
||||
session.query(Product)
|
||||
.filter(
|
||||
or_(
|
||||
Product.bar_code.ilike(f"%{string}%"),
|
||||
@@ -79,14 +64,11 @@ def search_product(
|
||||
)
|
||||
else:
|
||||
product_list = (
|
||||
sql_session.query(Product)
|
||||
session.query(Product)
|
||||
.filter(
|
||||
or_(
|
||||
Product.bar_code.ilike(f"%{string}%"),
|
||||
and_(
|
||||
Product.name.ilike(f"%{string}%"),
|
||||
not_(Product.hidden),
|
||||
),
|
||||
and_(Product.name.ilike(f"%{string}%"), Product.hidden is False),
|
||||
)
|
||||
)
|
||||
.all()
|
||||
@@ -94,7 +76,7 @@ def search_product(
|
||||
return product_list
|
||||
|
||||
|
||||
def system_user_exists(username: str) -> bool:
|
||||
def system_user_exists(username):
|
||||
try:
|
||||
pwd.getpwnam(username)
|
||||
except KeyError:
|
||||
@@ -105,7 +87,7 @@ def system_user_exists(username: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def guess_data_type(string: str) -> Literal["card", "rfid", "bar_code", "username"] | None:
|
||||
def guess_data_type(string):
|
||||
if string.startswith("ntnu") and string[4:].isdigit():
|
||||
return "card"
|
||||
if string.isdigit() and len(string) == 10:
|
||||
@@ -119,11 +101,7 @@ def guess_data_type(string: str) -> Literal["card", "rfid", "bar_code", "usernam
|
||||
return None
|
||||
|
||||
|
||||
def argmax(
|
||||
d,
|
||||
all: bool = False,
|
||||
value: Callable[[Any], Any] | None = None,
|
||||
):
|
||||
def argmax(d, all=False, value=None):
|
||||
maxarg = None
|
||||
if value is not None:
|
||||
dd = d
|
||||
@@ -138,7 +116,7 @@ def argmax(
|
||||
return maxarg
|
||||
|
||||
|
||||
def less(string: str) -> None:
|
||||
def less(string):
|
||||
"""
|
||||
Run less with string as input; wait until it finishes.
|
||||
"""
|
||||
|
||||
@@ -1,98 +1,96 @@
|
||||
import os
|
||||
import datetime
|
||||
|
||||
# import barcode
|
||||
# from brother_ql.brother_ql_create import create_label
|
||||
# from brother_ql.raster import BrotherQLRaster
|
||||
# from brother_ql.backends import backend_factory
|
||||
# from brother_ql.labels import ALL_LABELS
|
||||
# from PIL import Image, ImageDraw, ImageFont
|
||||
import barcode
|
||||
from brother_ql import BrotherQLRaster, create_label
|
||||
from brother_ql.backends import backend_factory
|
||||
from brother_ql.devicedependent import label_type_specs
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# from .barcode_helpers import BrotherLabelWriter
|
||||
from .barcode_helpers import BrotherLabelWriter
|
||||
|
||||
|
||||
# def print_name_label(
|
||||
# text,
|
||||
# margin=10,
|
||||
# rotate=False,
|
||||
# label_type="62",
|
||||
# printer_type="QL-700",
|
||||
# ):
|
||||
# label = next([l for l in ALL_LABELS if l.identifier == label_type])
|
||||
# if not rotate:
|
||||
# width, height = label.dots_printable
|
||||
# else:
|
||||
# height, width = label.dots_printable
|
||||
def print_name_label(
|
||||
text,
|
||||
margin=10,
|
||||
rotate=False,
|
||||
label_type="62",
|
||||
printer_type="QL-700",
|
||||
):
|
||||
if not rotate:
|
||||
width, height = label_type_specs[label_type]["dots_printable"]
|
||||
else:
|
||||
height, width = label_type_specs[label_type]["dots_printable"]
|
||||
|
||||
# font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ChopinScript.ttf")
|
||||
# fs = 2000
|
||||
# tw, th = width, height
|
||||
# if width == 0:
|
||||
# while th + 2 * margin > height:
|
||||
# font = ImageFont.truetype(font_path, fs)
|
||||
# tw, th = font.getsize(text)
|
||||
# fs -= 1
|
||||
# width = tw + 2 * margin
|
||||
# elif height == 0:
|
||||
# while tw + 2 * margin > width:
|
||||
# font = ImageFont.truetype(font_path, fs)
|
||||
# tw, th = font.getsize(text)
|
||||
# fs -= 1
|
||||
# height = th + 2 * margin
|
||||
# else:
|
||||
# while tw + 2 * margin > width or th + 2 * margin > height:
|
||||
# font = ImageFont.truetype(font_path, fs)
|
||||
# tw, th = font.getsize(text)
|
||||
# fs -= 1
|
||||
font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ChopinScript.ttf")
|
||||
fs = 2000
|
||||
tw, th = width, height
|
||||
if width == 0:
|
||||
while th + 2 * margin > height:
|
||||
font = ImageFont.truetype(font_path, fs)
|
||||
tw, th = font.getsize(text)
|
||||
fs -= 1
|
||||
width = tw + 2 * margin
|
||||
elif height == 0:
|
||||
while tw + 2 * margin > width:
|
||||
font = ImageFont.truetype(font_path, fs)
|
||||
tw, th = font.getsize(text)
|
||||
fs -= 1
|
||||
height = th + 2 * margin
|
||||
else:
|
||||
while tw + 2 * margin > width or th + 2 * margin > height:
|
||||
font = ImageFont.truetype(font_path, fs)
|
||||
tw, th = font.getsize(text)
|
||||
fs -= 1
|
||||
|
||||
# xp = (width // 2) - (tw // 2)
|
||||
# yp = (height // 2) - (th // 2)
|
||||
xp = (width // 2) - (tw // 2)
|
||||
yp = (height // 2) - (th // 2)
|
||||
|
||||
# im = Image.new("RGB", (width, height), (255, 255, 255))
|
||||
# dr = ImageDraw.Draw(im)
|
||||
im = Image.new("RGB", (width, height), (255, 255, 255))
|
||||
dr = ImageDraw.Draw(im)
|
||||
|
||||
# dr.text((xp, yp), text, fill=(0, 0, 0), font=font)
|
||||
# now = datetime.datetime.now()
|
||||
# date = now.strftime("%Y-%m-%d")
|
||||
# dr.text((0, 0), date, fill=(0, 0, 0))
|
||||
dr.text((xp, yp), text, fill=(0, 0, 0), font=font)
|
||||
now = datetime.datetime.now()
|
||||
date = now.strftime("%Y-%m-%d")
|
||||
dr.text((0, 0), date, fill=(0, 0, 0))
|
||||
|
||||
# base_path = os.path.dirname(os.path.realpath(__file__))
|
||||
# fn = os.path.join(base_path, "bar_codes", text + ".png")
|
||||
base_path = os.path.dirname(os.path.realpath(__file__))
|
||||
fn = os.path.join(base_path, "bar_codes", text + ".png")
|
||||
|
||||
# im.save(fn, "PNG")
|
||||
# print_image(fn, printer_type, label_type)
|
||||
im.save(fn, "PNG")
|
||||
print_image(fn, printer_type, label_type)
|
||||
|
||||
|
||||
# def print_bar_code(
|
||||
# barcode_value,
|
||||
# barcode_text,
|
||||
# barcode_type="ean13",
|
||||
# rotate=False,
|
||||
# printer_type="QL-700",
|
||||
# label_type="62",
|
||||
# ):
|
||||
# bar_coder = barcode.get_barcode_class(barcode_type)
|
||||
# wr = BrotherLabelWriter(typ=label_type, rot=rotate, text=barcode_text, max_height=1000)
|
||||
def print_bar_code(
|
||||
barcode_value,
|
||||
barcode_text,
|
||||
barcode_type="ean13",
|
||||
rotate=False,
|
||||
printer_type="QL-700",
|
||||
label_type="62",
|
||||
):
|
||||
bar_coder = barcode.get_barcode_class(barcode_type)
|
||||
wr = BrotherLabelWriter(typ=label_type, rot=rotate, text=barcode_text, max_height=1000)
|
||||
|
||||
# test = bar_coder(barcode_value, writer=wr)
|
||||
# base_path = os.path.dirname(os.path.realpath(__file__))
|
||||
# fn = test.save(os.path.join(base_path, "bar_codes", barcode_value))
|
||||
# print_image(fn, printer_type, label_type)
|
||||
test = bar_coder(barcode_value, writer=wr)
|
||||
base_path = os.path.dirname(os.path.realpath(__file__))
|
||||
fn = test.save(os.path.join(base_path, "bar_codes", barcode_value))
|
||||
print_image(fn, printer_type, label_type)
|
||||
|
||||
|
||||
# def print_image(fn, printer_type="QL-700", label_type="62"):
|
||||
# qlr = BrotherQLRaster(printer_type)
|
||||
# qlr.exception_on_warning = True
|
||||
# create_label(qlr, fn, label_type, threshold=70, cut=True)
|
||||
def print_image(fn, printer_type="QL-700", label_type="62"):
|
||||
qlr = BrotherQLRaster(printer_type)
|
||||
qlr.exception_on_warning = True
|
||||
create_label(qlr, fn, label_type, threshold=70, cut=True)
|
||||
|
||||
# be = backend_factory("pyusb")
|
||||
# list_available_devices = be["list_available_devices"]
|
||||
# BrotherQLBackend = be["backend_class"]
|
||||
be = backend_factory("pyusb")
|
||||
list_available_devices = be["list_available_devices"]
|
||||
BrotherQLBackend = be["backend_class"]
|
||||
|
||||
# ad = list_available_devices()
|
||||
# assert ad
|
||||
# string_descr = ad[0]["string_descr"]
|
||||
ad = list_available_devices()
|
||||
assert ad
|
||||
string_descr = ad[0]["string_descr"]
|
||||
|
||||
# printer = BrotherQLBackend(string_descr)
|
||||
printer = BrotherQLBackend(string_descr)
|
||||
|
||||
# printer.write(qlr.data)
|
||||
printer.write(qlr.data)
|
||||
|
||||
@@ -4,18 +4,17 @@
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .helpers import *
|
||||
from ..models import Transaction
|
||||
from ..db import Session
|
||||
|
||||
|
||||
def getUser(sql_session: Session):
|
||||
assert sql_session is not None
|
||||
def getUser():
|
||||
while 1:
|
||||
string = input("user? ")
|
||||
user = search_user(string, sql_session)
|
||||
sql_session.close()
|
||||
session = Session()
|
||||
user = search_user(string, session)
|
||||
session.close()
|
||||
if not isinstance(user, list):
|
||||
return user.name
|
||||
i = 0
|
||||
@@ -38,11 +37,12 @@ def getUser(sql_session: Session):
|
||||
return user[n].name
|
||||
|
||||
|
||||
def getProduct(sql_session: Session):
|
||||
assert sql_session is not None
|
||||
def getProduct():
|
||||
while 1:
|
||||
string = input("product? ")
|
||||
product = search_product(string, sql_session)
|
||||
session = Session()
|
||||
product = search_product(string, session)
|
||||
session.close()
|
||||
if not isinstance(product, list):
|
||||
return product.name
|
||||
i = 0
|
||||
@@ -76,8 +76,12 @@ class Database:
|
||||
personDatoVerdi = defaultdict(list) # dict->array
|
||||
personUkedagVerdi = defaultdict(list)
|
||||
# for global
|
||||
personPosTransactions = {} # personPosTransactions[trygvrad] == 100 #trygvrad har lagt 100kr i boksen
|
||||
personNegTransactions = {} # personNegTransactions[trygvrad» == 70 #trygvrad har tatt 70kr fra boksen
|
||||
personPosTransactions = (
|
||||
{}
|
||||
) # personPosTransactions[trygvrad] == 100 #trygvrad har lagt 100kr i boksen
|
||||
personNegTransactions = (
|
||||
{}
|
||||
) # personNegTransactions[trygvrad» == 70 #trygvrad har tatt 70kr fra boksen
|
||||
globalVareAntall = {} # globalVareAntall[Oreo] == 3
|
||||
globalVareVerdi = {} # globalVareVerdi[Oreo] == 30 #[kr]
|
||||
globalPersonAntall = {} # globalPersonAntall[trygvrad] == 3
|
||||
@@ -238,12 +242,12 @@ def addLineToDatabase(database, inputLine):
|
||||
return database
|
||||
|
||||
|
||||
def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session):
|
||||
assert sql_session is not None
|
||||
def buildDatabaseFromDb(inputType, inputProduct, inputUser):
|
||||
sdate = input("enter start date (yyyy-mm-dd)? ")
|
||||
edate = input("enter end date (yyyy-mm-dd)? ")
|
||||
print("building database...")
|
||||
transaction_list = sql_session.query(Transaction).all()
|
||||
session = Session()
|
||||
transaction_list = session.query(Transaction).all()
|
||||
inputLine = InputLine(inputUser, inputProduct, inputType)
|
||||
startDate = getDateDb(transaction_list[0].time, sdate)
|
||||
endDate = getDateDb(transaction_list[-1].time, edate)
|
||||
@@ -275,7 +279,7 @@ def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session
|
||||
print("saving as default.dibblerlog...", end=" ")
|
||||
f = open("default.dibblerlog", "w")
|
||||
line_format = "%s|%s|%s|%s|%s|%s\n"
|
||||
transaction_list = sql_session.query(Transaction).all()
|
||||
transaction_list = session.query(Transaction).all()
|
||||
for transaction in transaction_list:
|
||||
if transaction.purchase:
|
||||
products = "¤".join([ent.product.name for ent in transaction.purchase.entries])
|
||||
@@ -290,6 +294,7 @@ def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session
|
||||
transaction.description,
|
||||
)
|
||||
f.write(line.encode("utf8"))
|
||||
session.close()
|
||||
f.close
|
||||
# bygg database.pengebeholdning
|
||||
if (inputType == 3) or (inputType == 4):
|
||||
@@ -465,8 +470,7 @@ def printGlobal(database, dateLine, n):
|
||||
)
|
||||
|
||||
|
||||
def alt4menuTextOnly(database, dateLine, sql_session: Session):
|
||||
assert sql_session is not None
|
||||
def alt4menuTextOnly(database, dateLine):
|
||||
n = 10
|
||||
while 1:
|
||||
print(
|
||||
@@ -477,12 +481,12 @@ def alt4menuTextOnly(database, dateLine, sql_session: Session):
|
||||
break
|
||||
elif inp == "1":
|
||||
try:
|
||||
printUser(database, dateLine, getUser(sql_session), n)
|
||||
printUser(database, dateLine, getUser(), n)
|
||||
except:
|
||||
print("\n\nSomething is not right, (last date prior to first date?)")
|
||||
elif inp == "2":
|
||||
try:
|
||||
printProduct(database, dateLine, getProduct(sql_session), n)
|
||||
printProduct(database, dateLine, getProduct(), n)
|
||||
except:
|
||||
print("\n\nSomething is not right, (last date prior to first date?)")
|
||||
elif inp == "3":
|
||||
@@ -494,16 +498,15 @@ def alt4menuTextOnly(database, dateLine, sql_session: Session):
|
||||
n = int(input("set number to show "))
|
||||
|
||||
|
||||
def statisticsTextOnly(sql_session: Session):
|
||||
assert sql_session is not None
|
||||
def statisticsTextOnly():
|
||||
inputType = 4
|
||||
product = ""
|
||||
user = ""
|
||||
print("\n0: from file, 1: from database, q:quit")
|
||||
inp = input("")
|
||||
if inp == "1":
|
||||
database, dateLine = buildDatabaseFromDb(inputType, product, user, sql_session)
|
||||
database, dateLine = buildDatabaseFromDb(inputType, product, user)
|
||||
elif inp == "0" or inp == "":
|
||||
database, dateLine = buildDatabaseFromFile("default.dibblerlog", inputType, product, user)
|
||||
if not inp == "q":
|
||||
alt4menuTextOnly(database, dateLine, sql_session)
|
||||
alt4menuTextOnly(database, dateLine)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import os
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.conf import load_config, config_db_string
|
||||
from dibbler.conf import config
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
@@ -12,9 +10,9 @@ parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
help="Path to the config file",
|
||||
type=Path,
|
||||
metavar="FILE",
|
||||
type=str,
|
||||
required=False,
|
||||
default=os.environ.get("DIBBLER_CONFIG_FILE", None)
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
@@ -25,36 +23,28 @@ subparsers = parser.add_subparsers(
|
||||
subparsers.add_parser("loop", help="Run the dibbler loop")
|
||||
subparsers.add_parser("create-db", help="Create the database")
|
||||
subparsers.add_parser("slabbedasker", help="Find out who is slabbedasker")
|
||||
subparsers.add_parser("seed-data", help="Fill with mock data")
|
||||
|
||||
|
||||
def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
load_config(args.config)
|
||||
|
||||
engine = create_engine(config_db_string())
|
||||
sql_session = Session(engine)
|
||||
if args.config is None:
|
||||
print("ERROR: no config was provided", file=sys.stderr)
|
||||
config.read(args.config)
|
||||
|
||||
if args.subcommand == "loop":
|
||||
import dibbler.subcommands.loop as loop
|
||||
|
||||
loop.main(sql_session)
|
||||
loop.main()
|
||||
|
||||
elif args.subcommand == "create-db":
|
||||
import dibbler.subcommands.makedb as makedb
|
||||
|
||||
makedb.main(engine)
|
||||
makedb.main()
|
||||
|
||||
elif args.subcommand == "slabbedasker":
|
||||
import dibbler.subcommands.slabbedasker as slabbedasker
|
||||
|
||||
slabbedasker.main(sql_session)
|
||||
|
||||
elif args.subcommand == "seed-data":
|
||||
import dibbler.subcommands.seed_test_data as seed_test_data
|
||||
|
||||
seed_test_data.main(sql_session)
|
||||
slabbedasker.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from math import ceil
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import (
|
||||
Product,
|
||||
@@ -10,13 +9,12 @@ from dibbler.models import (
|
||||
Transaction,
|
||||
User,
|
||||
)
|
||||
|
||||
from .helpermenus import Menu
|
||||
|
||||
|
||||
class AddStockMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Add stock and adjust credit", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Add stock and adjust credit", uses_db=True)
|
||||
self.help_text = """
|
||||
Enter what you have bought for PVVVV here, along with your user name and how
|
||||
much money you're due in credits for the purchase when prompted.\n"""
|
||||
@@ -111,12 +109,7 @@ much money you're due in credits for the purchase when prompted.\n"""
|
||||
print(f"{self.products[product][0]}".rjust(width - len(product.name)))
|
||||
print(width * "-")
|
||||
|
||||
def add_thing_to_pending(
|
||||
self,
|
||||
thing: User | Product,
|
||||
amount: int,
|
||||
price: int,
|
||||
):
|
||||
def add_thing_to_pending(self, thing, amount, price):
|
||||
if isinstance(thing, User):
|
||||
self.users.append(thing)
|
||||
elif thing in list(self.products.keys()):
|
||||
@@ -158,10 +151,10 @@ much money you're due in credits for the purchase when prompted.\n"""
|
||||
PurchaseEntry(purchase, product, -self.products[product][0])
|
||||
|
||||
purchase.perform_soft_purchase(-self.price, round_up=False)
|
||||
self.sql_session.add(purchase)
|
||||
self.session.add(purchase)
|
||||
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print("Success! Transaction performed:")
|
||||
# self.print_info()
|
||||
for user in self.users:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import sqlalchemy
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.conf import config
|
||||
from dibbler.models import (
|
||||
@@ -14,11 +13,10 @@ from .helpermenus import Menu
|
||||
|
||||
|
||||
class BuyMenu(Menu):
|
||||
superfast_mode: bool
|
||||
purchase: Purchase
|
||||
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Buy", sql_session)
|
||||
def __init__(self, session=None):
|
||||
Menu.__init__(self, "Buy", uses_db=True)
|
||||
if session:
|
||||
self.session = session
|
||||
self.superfast_mode = False
|
||||
self.help_text = """
|
||||
Each purchase may contain one or more products and one or more buyers.
|
||||
@@ -30,7 +28,7 @@ addition, and you can type 'what' at any time to redisplay it.
|
||||
When finished, write an empty line to confirm the purchase.\n"""
|
||||
|
||||
@staticmethod
|
||||
def credit_check(user: User):
|
||||
def credit_check(user):
|
||||
"""
|
||||
|
||||
:param user:
|
||||
@@ -39,13 +37,9 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
"""
|
||||
assert isinstance(user, User)
|
||||
|
||||
return user.credit > config["limits"]["low_credit_warning_limit"]
|
||||
return user.credit > config.getint("limits", "low_credit_warning_limit")
|
||||
|
||||
def low_credit_warning(
|
||||
self,
|
||||
user: User,
|
||||
timeout: bool = False,
|
||||
):
|
||||
def low_credit_warning(self, user, timeout=False):
|
||||
assert isinstance(user, User)
|
||||
|
||||
print("***********************************************************************")
|
||||
@@ -64,7 +58,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
print("***********************************************************************")
|
||||
print("")
|
||||
print(
|
||||
f"USER {user.name} HAS LOWER CREDIT THAN {config['limits']['low_credit_warning_limit']:d}."
|
||||
f"USER {user.name} HAS LOWER CREDIT THAN {config.getint('limits', 'low_credit_warning_limit'):d}."
|
||||
)
|
||||
print("THIS PURCHASE WILL CHARGE YOUR CREDIT TWICE AS MUCH.")
|
||||
print("CONSIDER PUTTING MONEY IN THE BOX TO AVOID THIS.")
|
||||
@@ -77,11 +71,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
else:
|
||||
return self.confirm(prompt=">", default=True)
|
||||
|
||||
def add_thing_to_purchase(
|
||||
self,
|
||||
thing: User | Product,
|
||||
amount: int = 1,
|
||||
) -> bool:
|
||||
def add_thing_to_purchase(self, thing, amount=1):
|
||||
if isinstance(thing, User):
|
||||
if thing.is_anonymous():
|
||||
print("---------------------------------------------")
|
||||
@@ -90,10 +80,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
print("---------------------------------------------")
|
||||
|
||||
if not self.credit_check(thing):
|
||||
if self.low_credit_warning(
|
||||
user=thing,
|
||||
timeout=self.superfast_mode,
|
||||
):
|
||||
if self.low_credit_warning(user=thing, timeout=self.superfast_mode):
|
||||
Transaction(thing, purchase=self.purchase, penalty=2)
|
||||
else:
|
||||
return False
|
||||
@@ -108,10 +95,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
PurchaseEntry(self.purchase, thing, amount)
|
||||
return True
|
||||
|
||||
def _execute(
|
||||
self,
|
||||
initial_contents: list[tuple[User | Product, int]] | None = None,
|
||||
):
|
||||
def _execute(self, initial_contents=None):
|
||||
self.print_header()
|
||||
self.purchase = Purchase()
|
||||
self.exit_confirm_msg = None
|
||||
@@ -183,9 +167,9 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
break
|
||||
|
||||
self.purchase.perform_purchase()
|
||||
self.sql_session.add(self.purchase)
|
||||
self.session.add(self.purchase)
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store purchase: {e}")
|
||||
else:
|
||||
@@ -194,9 +178,9 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
for t in self.purchase.transactions:
|
||||
if not t.user.is_anonymous():
|
||||
print(f"User {t.user.name}'s credit is now {t.user.credit:d} kr")
|
||||
if t.user.credit < config["limits"]["low_credit_warning_limit"]:
|
||||
if t.user.credit < config.getint("limits", "low_credit_warning_limit"):
|
||||
print(
|
||||
f"USER {t.user.name} HAS LOWER CREDIT THAN {config['limits']['low_credit_warning_limit']:d},",
|
||||
f'USER {t.user.name} HAS LOWER CREDIT THAN {config.getint("limits", "low_credit_warning_limit"):d},',
|
||||
"AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import sqlalchemy
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import User, Product
|
||||
from .helpermenus import Menu, Selector
|
||||
|
||||
@@ -16,8 +14,8 @@ __all__ = [
|
||||
|
||||
|
||||
class AddUserMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Add user", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Add user", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
@@ -30,9 +28,9 @@ class AddUserMenu(Menu):
|
||||
cardnum = cardnum.lower()
|
||||
rfid = self.input_str("RFID (optional)", regex=User.rfid_re, length_range=(0, 10))
|
||||
user = User(username, cardnum, rfid)
|
||||
self.sql_session.add(user)
|
||||
self.session.add(user)
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print(f"User {username} stored")
|
||||
except sqlalchemy.exc.IntegrityError as e:
|
||||
print(f"Could not store user {username}: {e}")
|
||||
@@ -40,8 +38,8 @@ class AddUserMenu(Menu):
|
||||
|
||||
|
||||
class EditUserMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Edit user", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Edit user", uses_db=True)
|
||||
self.help_text = """
|
||||
The only editable part of a user is its card number and rfid.
|
||||
|
||||
@@ -71,7 +69,7 @@ user, then rfid (write an empty line to remove the card number or rfid).
|
||||
empty_string_is_none=True,
|
||||
)
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print(f"User {user.name} stored")
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store user {user.name}: {e}")
|
||||
@@ -79,8 +77,8 @@ user, then rfid (write an empty line to remove the card number or rfid).
|
||||
|
||||
|
||||
class AddProductMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Add product", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Add product", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
@@ -88,9 +86,9 @@ class AddProductMenu(Menu):
|
||||
name = self.input_str("Name", regex=Product.name_re, length_range=(1, Product.name_length))
|
||||
price = self.input_int("Price", 1, 100000)
|
||||
product = Product(bar_code, name, price)
|
||||
self.sql_session.add(product)
|
||||
self.session.add(product)
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print(f"Product {name} stored")
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store product {name}: {e}")
|
||||
@@ -98,8 +96,8 @@ class AddProductMenu(Menu):
|
||||
|
||||
|
||||
class EditProductMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Edit product", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Edit product", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
@@ -108,7 +106,6 @@ class EditProductMenu(Menu):
|
||||
while True:
|
||||
selector = Selector(
|
||||
f"Do what with {product.name}?",
|
||||
sql_session=self.sql_session,
|
||||
items=[
|
||||
("name", "Edit name"),
|
||||
("price", "Edit price"),
|
||||
@@ -138,7 +135,7 @@ class EditProductMenu(Menu):
|
||||
product.hidden = self.confirm(f"Hidden(currently {product.hidden})", default=False)
|
||||
elif what == "store":
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print(f"Product {product.name} stored")
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store product {product.name}: {e}")
|
||||
@@ -152,8 +149,8 @@ class EditProductMenu(Menu):
|
||||
|
||||
|
||||
class AdjustStockMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Adjust stock", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Adjust stock", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
@@ -171,7 +168,7 @@ class AdjustStockMenu(Menu):
|
||||
product.stock += add_stock
|
||||
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print("Stock is now stored")
|
||||
self.pause()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
@@ -182,13 +179,13 @@ class AdjustStockMenu(Menu):
|
||||
|
||||
|
||||
class CleanupStockMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Stock Cleanup", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Stock Cleanup", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
|
||||
products = self.sql_session.query(Product).filter(Product.stock != 0).all()
|
||||
products = self.session.query(Product).filter(Product.stock != 0).all()
|
||||
|
||||
print("Every product in stock will be printed.")
|
||||
print("Entering no value will keep current stock or set it to 0 if it is negative.")
|
||||
@@ -202,12 +199,12 @@ class CleanupStockMenu(Menu):
|
||||
for product in products:
|
||||
oldstock = product.stock
|
||||
product.stock = self.input_int(product.name, 0, 10000, default=max(0, oldstock))
|
||||
self.sql_session.add(product)
|
||||
self.session.add(product)
|
||||
if oldstock != product.stock:
|
||||
changed_products.append((product, oldstock))
|
||||
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print("New stocks are now stored.")
|
||||
self.pause()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .helpermenus import Menu, MessageMenu
|
||||
from .helpermenus import MessageMenu, Menu
|
||||
|
||||
|
||||
class FAQMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Frequently Asked Questions", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Frequently Asked Questions")
|
||||
self.items = [
|
||||
MessageMenu(
|
||||
"What is the meaning with this program?",
|
||||
@@ -18,25 +17,19 @@ class FAQMenu(Menu):
|
||||
|
||||
Dibbler stores a "credit" amount for each user. When you register a
|
||||
purchase in Dibbler, this amount is decreased. To increase your
|
||||
credit, purchase products for dibbler, and register them using "Add
|
||||
stock and adjust credit".
|
||||
credit, purchase products for dibbler, and register them using "Add
|
||||
stock and adjust credit".
|
||||
Alternatively, add money to the money box and use "Adjust credit" to
|
||||
tell Dibbler about it.
|
||||
""",
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu(
|
||||
"Can I still pay for stuff using cash?",
|
||||
"""
|
||||
Please put money in the money box and use "Adjust Credit" so that
|
||||
Please put money in the money box and use "Adjust Credit" so that
|
||||
dibbler can keep track of credit and purchases.""",
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu(
|
||||
"How do I exit from a submenu/dialog/thing?",
|
||||
'Type "exit", "q", or ^d.',
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu("How do I exit from a submenu/dialog/thing?", 'Type "exit", "q", or ^d.'),
|
||||
MessageMenu(
|
||||
'What does "." mean?',
|
||||
"""
|
||||
@@ -48,7 +41,6 @@ class FAQMenu(Menu):
|
||||
line containing only a period, you should read the lines above and
|
||||
then press enter to continue.
|
||||
""",
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu(
|
||||
"Why is the user interface so terribly unintuitive?",
|
||||
@@ -60,30 +52,25 @@ class FAQMenu(Menu):
|
||||
|
||||
Answer #3: YOU are unintuitive.
|
||||
""",
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu(
|
||||
"Why is there no help command?",
|
||||
'There is. Have you tried typing "help"?',
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu(
|
||||
'Where are the easter eggs? I tried saying "moo", but nothing happened.',
|
||||
'Don\'t say "moo".',
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu(
|
||||
"Why does the program speak English when all the users are Norwegians?",
|
||||
"Godt spørsmål. Det virket sikkert som en god idé der og da.",
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu(
|
||||
"Why does the screen have strange colours?",
|
||||
"""
|
||||
Type "c" on the main menu to change the colours of the display, or
|
||||
Type "c" on the main menu to change the colours of the display, or
|
||||
"cs" if you are a boring person.
|
||||
""",
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu(
|
||||
"I found a bug; is there a reward?",
|
||||
@@ -114,7 +101,6 @@ class FAQMenu(Menu):
|
||||
6. Type "restart" in Dibbler to replace the running process by a new
|
||||
one using the updated files.
|
||||
""",
|
||||
sql_session,
|
||||
),
|
||||
MessageMenu(
|
||||
"My question isn't listed here; what do I do?",
|
||||
@@ -139,6 +125,5 @@ class FAQMenu(Menu):
|
||||
5. Type "restart" in Dibbler to replace the running process by a new
|
||||
one using the updated files.
|
||||
""",
|
||||
sql_session,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,57 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import re
|
||||
import sys
|
||||
from select import select
|
||||
from typing import Any, Callable, Iterable, Literal, Self
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.db import Session
|
||||
from dibbler.models import User
|
||||
from dibbler.lib.helpers import (
|
||||
argmax,
|
||||
guess_data_type,
|
||||
search_product,
|
||||
search_user,
|
||||
search_product,
|
||||
guess_data_type,
|
||||
argmax,
|
||||
)
|
||||
from dibbler.models import Product, User
|
||||
|
||||
exit_commands: list[str] = ["exit", "abort", "quit", "bye", "eat flaming death", "q"]
|
||||
help_commands: list[str] = ["help", "?"]
|
||||
context_commands: list[str] = ["what", "??"]
|
||||
local_help_commands: list[str] = ["help!", "???"]
|
||||
exit_commands = ["exit", "abort", "quit", "bye", "eat flaming death", "q"]
|
||||
help_commands = ["help", "?"]
|
||||
context_commands = ["what", "??"]
|
||||
local_help_commands = ["help!", "???"]
|
||||
|
||||
|
||||
class ExitMenuException(Exception):
|
||||
class ExitMenu(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Menu(object):
|
||||
name: str
|
||||
sql_session: Session
|
||||
items: list[Self | tuple | str]
|
||||
prompt: str | None
|
||||
end_prompt: str | None
|
||||
return_index: bool
|
||||
exit_msg: str | None
|
||||
exit_confirm_msg: str | None
|
||||
exit_disallowed_msg: str | None
|
||||
help_text: str | None
|
||||
context: str | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
sql_session: Session,
|
||||
items: list[Self | tuple[Any, str] | str] | None = None,
|
||||
prompt: str | None = None,
|
||||
end_prompt: str | None = "> ",
|
||||
return_index: bool = True,
|
||||
exit_msg: str | None = None,
|
||||
exit_confirm_msg: str | None = None,
|
||||
exit_disallowed_msg: str | None = None,
|
||||
help_text: str | None = None,
|
||||
name,
|
||||
items=None,
|
||||
prompt=None,
|
||||
end_prompt="> ",
|
||||
return_index=True,
|
||||
exit_msg=None,
|
||||
exit_confirm_msg=None,
|
||||
exit_disallowed_msg=None,
|
||||
help_text=None,
|
||||
uses_db=False,
|
||||
):
|
||||
self.name: str = name
|
||||
self.sql_session: Session = sql_session
|
||||
self.name = name
|
||||
self.items = items if items is not None else []
|
||||
self.prompt = prompt
|
||||
self.end_prompt = end_prompt
|
||||
@@ -61,54 +48,46 @@ class Menu(object):
|
||||
self.exit_disallowed_msg = exit_disallowed_msg
|
||||
self.help_text = help_text
|
||||
self.context = None
|
||||
self.uses_db = uses_db
|
||||
self.session = None
|
||||
|
||||
assert name is not None
|
||||
assert self.sql_session is not None
|
||||
|
||||
def exit_menu(self) -> None:
|
||||
def exit_menu(self):
|
||||
if self.exit_disallowed_msg is not None:
|
||||
print(self.exit_disallowed_msg)
|
||||
return
|
||||
if self.exit_confirm_msg is not None:
|
||||
if not self.confirm(self.exit_confirm_msg, default=True):
|
||||
return
|
||||
raise ExitMenuException()
|
||||
raise ExitMenu()
|
||||
|
||||
def at_exit(self) -> None:
|
||||
def at_exit(self):
|
||||
if self.exit_msg:
|
||||
print(self.exit_msg)
|
||||
|
||||
def set_context(
|
||||
self,
|
||||
string: str | None,
|
||||
display: bool = True,
|
||||
) -> None:
|
||||
def set_context(self, string, display=True):
|
||||
self.context = string
|
||||
if self.context is not None and display:
|
||||
print(self.context)
|
||||
|
||||
def add_to_context(self, string: str) -> None:
|
||||
if self.context is not None:
|
||||
self.context += string
|
||||
else:
|
||||
self.context = string
|
||||
def add_to_context(self, string):
|
||||
self.context += string
|
||||
|
||||
def printc(self, string: str) -> None:
|
||||
def printc(self, string):
|
||||
print(string)
|
||||
if self.context is None:
|
||||
self.context = string
|
||||
else:
|
||||
self.context += "\n" + string
|
||||
|
||||
def show_context(self) -> None:
|
||||
def show_context(self):
|
||||
print(self.header())
|
||||
if self.context is not None:
|
||||
print(self.context)
|
||||
|
||||
def item_is_submenu(self, i: int) -> bool:
|
||||
def item_is_submenu(self, i):
|
||||
return isinstance(self.items[i], Menu)
|
||||
|
||||
def item_name(self, i: int) -> str:
|
||||
def item_name(self, i):
|
||||
if self.item_is_submenu(i):
|
||||
return self.items[i].name
|
||||
elif isinstance(self.items[i], tuple):
|
||||
@@ -116,7 +95,7 @@ class Menu(object):
|
||||
else:
|
||||
return self.items[i]
|
||||
|
||||
def item_value(self, i: int):
|
||||
def item_value(self, i):
|
||||
if isinstance(self.items[i], tuple):
|
||||
return self.items[i][0]
|
||||
if self.return_index:
|
||||
@@ -125,11 +104,11 @@ class Menu(object):
|
||||
|
||||
def input_str(
|
||||
self,
|
||||
prompt: str | None = None,
|
||||
end_prompt: str | None = None,
|
||||
regex: str | None = None,
|
||||
prompt=None,
|
||||
end_prompt=None,
|
||||
regex=None,
|
||||
length_range=(None, None),
|
||||
empty_string_is_none: bool = False,
|
||||
empty_string_is_none=False,
|
||||
timeout=None,
|
||||
default=None,
|
||||
):
|
||||
@@ -198,7 +177,7 @@ class Menu(object):
|
||||
continue
|
||||
return result
|
||||
|
||||
def special_input_options(self, result) -> bool:
|
||||
def special_input_options(self, result):
|
||||
"""
|
||||
Handles special, magic input for input_str
|
||||
|
||||
@@ -208,7 +187,7 @@ class Menu(object):
|
||||
"""
|
||||
return False
|
||||
|
||||
def special_input_choice(self, in_str: str) -> bool:
|
||||
def special_input_choice(self, in_str):
|
||||
"""
|
||||
Handle choices which are not simply menu items.
|
||||
|
||||
@@ -218,12 +197,7 @@ class Menu(object):
|
||||
"""
|
||||
return False
|
||||
|
||||
def input_choice(
|
||||
self,
|
||||
number_of_choices: int,
|
||||
prompt: str | None = None,
|
||||
end_prompt: str | None = None,
|
||||
):
|
||||
def input_choice(self, number_of_choices, prompt=None, end_prompt=None):
|
||||
while True:
|
||||
result = self.input_str(prompt, end_prompt)
|
||||
if result == "":
|
||||
@@ -238,7 +212,7 @@ class Menu(object):
|
||||
if not self.special_input_choice(result):
|
||||
self.invalid_menu_choice(result)
|
||||
|
||||
def invalid_menu_choice(self, in_str: str):
|
||||
def invalid_menu_choice(self, in_str):
|
||||
print("Please enter a valid choice.")
|
||||
|
||||
def input_int(
|
||||
@@ -278,40 +252,32 @@ class Menu(object):
|
||||
except ValueError:
|
||||
print("Please enter an integer")
|
||||
|
||||
def input_user(
|
||||
self,
|
||||
prompt: str | None = None,
|
||||
end_prompt: str | None = None,
|
||||
) -> User:
|
||||
def input_user(self, prompt=None, end_prompt=None):
|
||||
user = None
|
||||
while user is None:
|
||||
user = self.retrieve_user(self.input_str(prompt, end_prompt))
|
||||
return user
|
||||
|
||||
def retrieve_user(self, search_str: str) -> User | None:
|
||||
def retrieve_user(self, search_str):
|
||||
return self.search_ui(search_user, search_str, "user")
|
||||
|
||||
def input_product(
|
||||
self,
|
||||
prompt: str | None = None,
|
||||
end_prompt: str | None = None,
|
||||
) -> Product:
|
||||
def input_product(self, prompt=None, end_prompt=None):
|
||||
product = None
|
||||
while product is None:
|
||||
product = self.retrieve_product(self.input_str(prompt, end_prompt))
|
||||
return product
|
||||
|
||||
def retrieve_product(self, search_str: str) -> Product | None:
|
||||
def retrieve_product(self, search_str):
|
||||
return self.search_ui(search_product, search_str, "product")
|
||||
|
||||
def input_thing(
|
||||
self,
|
||||
prompt: str | None = None,
|
||||
end_prompt: str | None = None,
|
||||
permitted_things: Iterable[str] = ("user", "product"),
|
||||
prompt=None,
|
||||
end_prompt=None,
|
||||
permitted_things=("user", "product"),
|
||||
add_nonexisting=(),
|
||||
empty_input_permitted: bool = False,
|
||||
find_hidden_products: bool = True,
|
||||
empty_input_permitted=False,
|
||||
find_hidden_products=True,
|
||||
):
|
||||
result = None
|
||||
while result is None:
|
||||
@@ -319,22 +285,19 @@ class Menu(object):
|
||||
if search_str == "" and empty_input_permitted:
|
||||
return None
|
||||
result = self.search_for_thing(
|
||||
search_str,
|
||||
permitted_things,
|
||||
add_nonexisting,
|
||||
find_hidden_products,
|
||||
search_str, permitted_things, add_nonexisting, find_hidden_products
|
||||
)
|
||||
return result
|
||||
|
||||
def input_multiple(
|
||||
self,
|
||||
prompt: str | None = None,
|
||||
end_prompt: str | None = None,
|
||||
permitted_things: Iterable[str] = ("user", "product"),
|
||||
prompt=None,
|
||||
end_prompt=None,
|
||||
permitted_things=("user", "product"),
|
||||
add_nonexisting=(),
|
||||
empty_input_permitted: bool = False,
|
||||
find_hidden_products: bool = True,
|
||||
) -> tuple[User | Product, int] | None:
|
||||
empty_input_permitted=False,
|
||||
find_hidden_products=True,
|
||||
):
|
||||
result = None
|
||||
num = 0
|
||||
while result is None:
|
||||
@@ -344,10 +307,7 @@ class Menu(object):
|
||||
return None
|
||||
else:
|
||||
result = self.search_for_thing(
|
||||
search_str,
|
||||
permitted_things,
|
||||
add_nonexisting,
|
||||
find_hidden_products,
|
||||
search_str, permitted_things, add_nonexisting, find_hidden_products
|
||||
)
|
||||
num = 1
|
||||
|
||||
@@ -369,19 +329,16 @@ class Menu(object):
|
||||
|
||||
def search_for_thing(
|
||||
self,
|
||||
search_str: str,
|
||||
search_str,
|
||||
permitted_things=("user", "product"),
|
||||
add_non_existing=(),
|
||||
find_hidden_products: bool = True,
|
||||
) -> User | Product | None:
|
||||
search_fun = {
|
||||
"user": search_user,
|
||||
"product": search_product,
|
||||
}
|
||||
find_hidden_products=True,
|
||||
):
|
||||
search_fun = {"user": search_user, "product": search_product}
|
||||
results = {}
|
||||
result_values = {}
|
||||
for thing in permitted_things:
|
||||
results[thing] = search_fun[thing](search_str, self.sql_session, find_hidden_products)
|
||||
results[thing] = search_fun[thing](search_str, self.session, find_hidden_products)
|
||||
result_values[thing] = self.search_result_value(results[thing])
|
||||
selected_thing = argmax(result_values)
|
||||
if not results[selected_thing]:
|
||||
@@ -396,14 +353,10 @@ class Menu(object):
|
||||
return self.search_add(search_str)
|
||||
# print('No match found for "%s".' % search_str)
|
||||
return None
|
||||
return self.search_ui2(
|
||||
search_str,
|
||||
results[selected_thing],
|
||||
selected_thing,
|
||||
)
|
||||
return self.search_ui2(search_str, results[selected_thing], selected_thing)
|
||||
|
||||
@staticmethod
|
||||
def search_result_value(result) -> Literal[0, 1, 2, 3]:
|
||||
def search_result_value(result):
|
||||
if result is None:
|
||||
return 0
|
||||
if not isinstance(result, list):
|
||||
@@ -414,19 +367,18 @@ class Menu(object):
|
||||
return 2
|
||||
return 1
|
||||
|
||||
def search_add(self, string: str) -> User | None:
|
||||
def search_add(self, string):
|
||||
type_guess = guess_data_type(string)
|
||||
if type_guess == "username":
|
||||
print(f'"{string}" looks like a username, but no such user exists.')
|
||||
if self.confirm(f"Create user {string}?"):
|
||||
user = User(string, None)
|
||||
self.sql_session.add(user)
|
||||
self.session.add(user)
|
||||
return user
|
||||
return None
|
||||
if type_guess == "card":
|
||||
selector = Selector(
|
||||
f'"{string}" looks like a card number, but no user with that card number exists.',
|
||||
self.sql_session,
|
||||
[
|
||||
("create", f"Create user with card number {string}"),
|
||||
("set", f"Set card number of an existing user to {string}"),
|
||||
@@ -440,7 +392,7 @@ class Menu(object):
|
||||
(1, 10),
|
||||
)
|
||||
user = User(username, string)
|
||||
self.sql_session.add(user)
|
||||
self.session.add(user)
|
||||
return user
|
||||
if selection == "set":
|
||||
user = self.input_user("User to set card number for")
|
||||
@@ -453,21 +405,11 @@ class Menu(object):
|
||||
print(f'"{string}" looks like the bar code for a product, but no such product exists.')
|
||||
return None
|
||||
|
||||
def search_ui(
|
||||
self,
|
||||
search_fun: Callable[[str, Session], list[Any] | Any],
|
||||
search_str: str,
|
||||
thing: str,
|
||||
) -> Any:
|
||||
result = search_fun(search_str, self.sql_session)
|
||||
def search_ui(self, search_fun, search_str, thing):
|
||||
result = search_fun(search_str, self.session)
|
||||
return self.search_ui2(search_str, result, thing)
|
||||
|
||||
def search_ui2(
|
||||
self,
|
||||
search_str: str,
|
||||
result: list[Any] | Any,
|
||||
thing: str,
|
||||
) -> Any:
|
||||
def search_ui2(self, search_str, result, thing):
|
||||
if not isinstance(result, list):
|
||||
return result
|
||||
if len(result) == 0:
|
||||
@@ -487,37 +429,21 @@ class Menu(object):
|
||||
else:
|
||||
select_header = f'{len(result):d} {thing}s matching "{search_str}"'
|
||||
select_items = result
|
||||
selector = Selector(
|
||||
select_header,
|
||||
self.sql_session,
|
||||
items=select_items,
|
||||
return_index=False,
|
||||
)
|
||||
selector = Selector(select_header, items=select_items, return_index=False)
|
||||
return selector.execute()
|
||||
|
||||
def confirm(
|
||||
self,
|
||||
prompt: str,
|
||||
end_prompt: str | None = None,
|
||||
default: bool | None = None,
|
||||
timeout: int | None = None,
|
||||
) -> bool:
|
||||
return ConfirmMenu(
|
||||
self.sql_session,
|
||||
prompt,
|
||||
end_prompt=None,
|
||||
default=default,
|
||||
timeout=timeout,
|
||||
).execute()
|
||||
@staticmethod
|
||||
def confirm(prompt, end_prompt=None, default=None, timeout=None):
|
||||
return ConfirmMenu(prompt, end_prompt=None, default=default, timeout=timeout).execute()
|
||||
|
||||
def header(self) -> str:
|
||||
def header(self):
|
||||
return f"[{self.name}]"
|
||||
|
||||
def print_header(self) -> None:
|
||||
def print_header(self):
|
||||
print("")
|
||||
print(self.header())
|
||||
|
||||
def pause(self) -> None:
|
||||
def pause(self):
|
||||
self.input_str(".", end_prompt="")
|
||||
|
||||
@staticmethod
|
||||
@@ -558,13 +484,16 @@ class Menu(object):
|
||||
def execute(self, **kwargs):
|
||||
self.set_context(None)
|
||||
try:
|
||||
if self.uses_db and not self.session:
|
||||
self.session = Session()
|
||||
return self._execute(**kwargs)
|
||||
except ExitMenuException:
|
||||
except ExitMenu:
|
||||
self.at_exit()
|
||||
return None
|
||||
finally:
|
||||
if self.sql_session is not None:
|
||||
self.sql_session = None
|
||||
if self.session is not None:
|
||||
self.session.close()
|
||||
self.session = None
|
||||
|
||||
def _execute(self, **kwargs):
|
||||
while True:
|
||||
@@ -585,17 +514,8 @@ class Menu(object):
|
||||
|
||||
|
||||
class MessageMenu(Menu):
|
||||
message: str
|
||||
pause_after_message: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
message: str,
|
||||
sql_session: Session,
|
||||
pause_after_message: bool = True,
|
||||
):
|
||||
super().__init__(name, sql_session)
|
||||
def __init__(self, name, message, pause_after_message=True):
|
||||
Menu.__init__(self, name)
|
||||
self.message = message.strip()
|
||||
self.pause_after_message = pause_after_message
|
||||
|
||||
@@ -608,17 +528,10 @@ class MessageMenu(Menu):
|
||||
|
||||
|
||||
class ConfirmMenu(Menu):
|
||||
def __init__(
|
||||
self,
|
||||
sql_session: Session,
|
||||
prompt: str = "confirm? ",
|
||||
end_prompt: str | None = ": ",
|
||||
default: bool | None = None,
|
||||
timeout: int | None = 0,
|
||||
):
|
||||
super().__init__(
|
||||
def __init__(self, prompt="confirm? ", end_prompt=": ", default=None, timeout=0):
|
||||
Menu.__init__(
|
||||
self,
|
||||
"question",
|
||||
sql_session,
|
||||
prompt=prompt,
|
||||
end_prompt=end_prompt,
|
||||
exit_disallowed_msg="Please answer yes or no",
|
||||
@@ -646,8 +559,7 @@ class ConfirmMenu(Menu):
|
||||
class Selector(Menu):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
sql_session: Session,
|
||||
name,
|
||||
items=None,
|
||||
prompt="select",
|
||||
return_index=True,
|
||||
@@ -657,22 +569,15 @@ class Selector(Menu):
|
||||
):
|
||||
if items is None:
|
||||
items = []
|
||||
super().__init__(
|
||||
name,
|
||||
sql_session,
|
||||
items,
|
||||
prompt,
|
||||
return_index=return_index,
|
||||
exit_msg=exit_msg,
|
||||
)
|
||||
Menu.__init__(self, name, items, prompt, return_index=return_index, exit_msg=exit_msg)
|
||||
|
||||
def header(self) -> str:
|
||||
def header(self):
|
||||
return self.name
|
||||
|
||||
def print_header(self) -> None:
|
||||
def print_header(self):
|
||||
print(self.header())
|
||||
|
||||
def local_help(self) -> None:
|
||||
def local_help(self):
|
||||
if self.help_text is None:
|
||||
print("This is a selection menu. Enter one of the listed numbers, or")
|
||||
print("'exit' to go out and do something else.")
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from dibbler.db import Session
|
||||
|
||||
from .buymenu import BuyMenu
|
||||
from .faq import FAQMenu
|
||||
@@ -20,10 +20,7 @@ def restart():
|
||||
|
||||
|
||||
class MainMenu(Menu):
|
||||
def __init__(self, sql_session: Session, **kwargs):
|
||||
super().__init__("Dibbler main menu", sql_session, **kwargs)
|
||||
|
||||
def special_input_choice(self, in_str: str) -> bool:
|
||||
def special_input_choice(self, in_str):
|
||||
mv = in_str.split()
|
||||
if len(mv) == 2 and mv[0].isdigit():
|
||||
num = int(mv[0])
|
||||
@@ -31,7 +28,7 @@ class MainMenu(Menu):
|
||||
else:
|
||||
num = 1
|
||||
item_name = in_str
|
||||
buy_menu = BuyMenu(self.sql_session)
|
||||
buy_menu = BuyMenu(Session())
|
||||
thing = buy_menu.search_for_thing(item_name, find_hidden_products=False)
|
||||
if thing:
|
||||
buy_menu.execute(initial_contents=[(thing, num)])
|
||||
@@ -39,9 +36,9 @@ class MainMenu(Menu):
|
||||
return True
|
||||
return False
|
||||
|
||||
def special_input_options(self, result: str) -> bool:
|
||||
def special_input_options(self, result):
|
||||
if result in faq_commands:
|
||||
FAQMenu(self.sql_session).execute()
|
||||
FAQMenu().execute()
|
||||
return True
|
||||
if result in restart_commands:
|
||||
if self.confirm("Restart Dibbler?"):
|
||||
@@ -66,5 +63,5 @@ class MainMenu(Menu):
|
||||
return True
|
||||
return False
|
||||
|
||||
def invalid_menu_choice(self, in_str: str) -> None:
|
||||
def invalid_menu_choice(self, in_str):
|
||||
print(self.show_context())
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import sqlalchemy
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.conf import config
|
||||
from dibbler.models import Transaction, Product, User
|
||||
@@ -9,8 +8,8 @@ from .helpermenus import Menu, Selector
|
||||
|
||||
|
||||
class TransferMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Transfer credit between users", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Transfer credit between users", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
@@ -27,10 +26,10 @@ class TransferMenu(Menu):
|
||||
t2 = Transaction(user2, -amount, f'transfer from {user1.name} "{comment}"')
|
||||
t1.perform_transaction()
|
||||
t2.perform_transaction()
|
||||
self.sql_session.add(t1)
|
||||
self.sql_session.add(t2)
|
||||
self.session.add(t1)
|
||||
self.session.add(t2)
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print(f"Transferred {amount:d} kr from {user1} to {user2}")
|
||||
print(f"User {user1}'s credit is now {user1.credit:d} kr")
|
||||
print(f"User {user2}'s credit is now {user2.credit:d} kr")
|
||||
@@ -41,8 +40,8 @@ class TransferMenu(Menu):
|
||||
|
||||
|
||||
class ShowUserMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Show user", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Show user", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
@@ -53,12 +52,11 @@ class ShowUserMenu(Menu):
|
||||
print(f"Credit: {user.credit} kr")
|
||||
selector = Selector(
|
||||
f"What do you want to know about {user.name}?",
|
||||
self.sql_session,
|
||||
items=[
|
||||
(
|
||||
"transactions",
|
||||
"Recent transactions (List of last "
|
||||
+ str(config["limits"]["user_recent_transaction_limit"])
|
||||
+ str(config.getint("limits", "user_recent_transaction_limit"))
|
||||
+ ")",
|
||||
),
|
||||
("products", f"Which products {user.name} has bought, and how many"),
|
||||
@@ -67,7 +65,7 @@ class ShowUserMenu(Menu):
|
||||
)
|
||||
what = selector.execute()
|
||||
if what == "transactions":
|
||||
self.print_transactions(user, config["limits"]["user_recent_transaction_limit"])
|
||||
self.print_transactions(user, config.getint("limits", "user_recent_transaction_limit"))
|
||||
elif what == "products":
|
||||
self.print_purchased_products(user)
|
||||
elif what == "transactions-all":
|
||||
@@ -76,7 +74,7 @@ class ShowUserMenu(Menu):
|
||||
print("What what?")
|
||||
|
||||
@staticmethod
|
||||
def print_transactions(user: User, limit: int | None = None) -> None:
|
||||
def print_transactions(user, limit=None):
|
||||
num_trans = len(user.transactions)
|
||||
if limit is None:
|
||||
limit = num_trans
|
||||
@@ -100,13 +98,13 @@ class ShowUserMenu(Menu):
|
||||
string += ")"
|
||||
if t.penalty > 1:
|
||||
string += f" * {t.penalty:d}x penalty applied"
|
||||
elif t.description is not None:
|
||||
else:
|
||||
string += t.description
|
||||
string += "\n"
|
||||
less(string)
|
||||
|
||||
@staticmethod
|
||||
def print_purchased_products(user: User) -> None:
|
||||
def print_purchased_products(user):
|
||||
products = []
|
||||
for ref in user.products:
|
||||
product = ref.product
|
||||
@@ -125,13 +123,13 @@ class ShowUserMenu(Menu):
|
||||
|
||||
|
||||
class UserListMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("User list", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "User list", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
user_list = self.sql_session.query(User).all()
|
||||
total_credit = self.sql_session.query(sqlalchemy.func.sum(User.credit)).first()[0]
|
||||
user_list = self.session.query(User).all()
|
||||
total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0]
|
||||
|
||||
line_format = "%-12s | %6s\n"
|
||||
hline = "---------------------\n"
|
||||
@@ -146,8 +144,8 @@ class UserListMenu(Menu):
|
||||
|
||||
|
||||
class AdjustCreditMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Adjust credit", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Adjust credit", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
@@ -166,9 +164,9 @@ class AdjustCreditMenu(Menu):
|
||||
description = "manually adjusted credit"
|
||||
transaction = Transaction(user, -amount, description)
|
||||
transaction.perform_transaction()
|
||||
self.sql_session.add(transaction)
|
||||
self.session.add(transaction)
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print(f"User {user.name}'s credit is now {user.credit:d} kr")
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store transaction: {e}")
|
||||
@@ -176,14 +174,14 @@ class AdjustCreditMenu(Menu):
|
||||
|
||||
|
||||
class ProductListMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Product list", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Product list", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
text = ""
|
||||
product_list = (
|
||||
self.sql_session.query(Product)
|
||||
self.session.query(Product)
|
||||
.filter(Product.hidden.is_(False))
|
||||
.order_by(Product.stock.desc())
|
||||
)
|
||||
@@ -206,8 +204,8 @@ class ProductListMenu(Menu):
|
||||
|
||||
|
||||
class ProductSearchMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Product search", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Product search", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import re
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.conf import config
|
||||
from dibbler.models import Product, User
|
||||
# from dibbler.lib.printer_helpers import print_bar_code, print_name_label
|
||||
from dibbler.lib.printer_helpers import print_bar_code, print_name_label
|
||||
|
||||
from .helpermenus import Menu
|
||||
|
||||
|
||||
class PrintLabelMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Print a label", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Print a label", uses_db=True)
|
||||
self.help_text = """
|
||||
Prints out a product bar code on the printer
|
||||
|
||||
@@ -21,31 +19,27 @@ Put it up somewhere in the vicinity.
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
|
||||
print("Printer menu is under renovation, please be patient")
|
||||
thing = self.input_thing("Product/User")
|
||||
|
||||
return
|
||||
|
||||
# thing = self.input_thing("Product/User")
|
||||
|
||||
# if isinstance(thing, Product):
|
||||
# if re.match(r"^[0-9]{13}$", thing.bar_code):
|
||||
# bar_type = "ean13"
|
||||
# elif re.match(r"^[0-9]{8}$", thing.bar_code):
|
||||
# bar_type = "ean8"
|
||||
# else:
|
||||
# bar_type = "code39"
|
||||
# print_bar_code(
|
||||
# thing.bar_code,
|
||||
# thing.name,
|
||||
# barcode_type=bar_type,
|
||||
# rotate=config["printer"]["rotate"],
|
||||
# printer_type="QL-700",
|
||||
# label_type=config.get("printer", "label_type"),
|
||||
# )
|
||||
# elif isinstance(thing, User):
|
||||
# print_name_label(
|
||||
# text=thing.name,
|
||||
# label_type=config["printer"]["label_type"],
|
||||
# rotate=config["printer"]["rotate"],
|
||||
# printer_type="QL-700",
|
||||
# )
|
||||
if isinstance(thing, Product):
|
||||
if re.match(r"^[0-9]{13}$", thing.bar_code):
|
||||
bar_type = "ean13"
|
||||
elif re.match(r"^[0-9]{8}$", thing.bar_code):
|
||||
bar_type = "ean8"
|
||||
else:
|
||||
bar_type = "code39"
|
||||
print_bar_code(
|
||||
thing.bar_code,
|
||||
thing.name,
|
||||
barcode_type=bar_type,
|
||||
rotate=config.getboolean("printer", "rotate"),
|
||||
printer_type="QL-700",
|
||||
label_type=config.get("printer", "label_type"),
|
||||
)
|
||||
elif isinstance(thing, User):
|
||||
print_name_label(
|
||||
text=thing.name,
|
||||
label_type=config.get("printer", "label_type"),
|
||||
rotate=config.getboolean("printer", "rotate"),
|
||||
printer_type="QL-700",
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from sqlalchemy import desc, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.lib.helpers import less
|
||||
from dibbler.models import PurchaseEntry, Product, User
|
||||
@@ -16,14 +15,14 @@ __all__ = [
|
||||
|
||||
|
||||
class ProductPopularityMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Products by popularity", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Products by popularity", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
text = ""
|
||||
sub = (
|
||||
self.sql_session.query(
|
||||
self.session.query(
|
||||
PurchaseEntry.product_id,
|
||||
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
||||
)
|
||||
@@ -32,7 +31,7 @@ class ProductPopularityMenu(Menu):
|
||||
.subquery()
|
||||
)
|
||||
product_list = (
|
||||
self.sql_session.query(Product, sub.c.purchase_count)
|
||||
self.session.query(Product, sub.c.purchase_count)
|
||||
.outerjoin((sub, Product.product_id == sub.c.product_id))
|
||||
.order_by(desc(sub.c.purchase_count))
|
||||
.filter(sub.c.purchase_count is not None)
|
||||
@@ -49,14 +48,14 @@ class ProductPopularityMenu(Menu):
|
||||
|
||||
|
||||
class ProductRevenueMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Products by revenue", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Products by revenue", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
text = ""
|
||||
sub = (
|
||||
self.sql_session.query(
|
||||
self.session.query(
|
||||
PurchaseEntry.product_id,
|
||||
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
||||
)
|
||||
@@ -65,7 +64,7 @@ class ProductRevenueMenu(Menu):
|
||||
.subquery()
|
||||
)
|
||||
product_list = (
|
||||
self.sql_session.query(Product, sub.c.purchase_count)
|
||||
self.session.query(Product, sub.c.purchase_count)
|
||||
.outerjoin((sub, Product.product_id == sub.c.product_id))
|
||||
.order_by(desc(sub.c.purchase_count * Product.price))
|
||||
.filter(sub.c.purchase_count is not None)
|
||||
@@ -87,22 +86,22 @@ class ProductRevenueMenu(Menu):
|
||||
|
||||
|
||||
class BalanceMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Total balance of PVVVV", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Total balance of PVVVV", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
text = ""
|
||||
total_value = 0
|
||||
product_list = self.sql_session.query(Product).filter(Product.stock > 0).all()
|
||||
product_list = self.session.query(Product).filter(Product.stock > 0).all()
|
||||
for p in product_list:
|
||||
total_value += p.stock * p.price
|
||||
|
||||
total_positive_credit = (
|
||||
self.sql_session.query(func.sum(User.credit)).filter(User.credit > 0).first()[0]
|
||||
self.session.query(func.sum(User.credit)).filter(User.credit > 0).first()[0]
|
||||
)
|
||||
total_negative_credit = (
|
||||
self.sql_session.query(func.sum(User.credit)).filter(User.credit < 0).first()[0]
|
||||
self.session.query(func.sum(User.credit)).filter(User.credit < 0).first()[0]
|
||||
)
|
||||
|
||||
total_credit = total_positive_credit + total_negative_credit
|
||||
@@ -120,8 +119,8 @@ class BalanceMenu(Menu):
|
||||
|
||||
|
||||
class LoggedStatisticsMenu(Menu):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__("Statistics from log", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Statistics from log", uses_db=True)
|
||||
|
||||
def _execute(self):
|
||||
statisticsTextOnly(self.sql_session)
|
||||
statisticsTextOnly()
|
||||
|
||||
@@ -36,19 +36,12 @@ class Product(Base):
|
||||
name_re = r".+"
|
||||
name_length = 45
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bar_code: str,
|
||||
name: str,
|
||||
price: int,
|
||||
stock: int = 0,
|
||||
hidden: bool = False,
|
||||
):
|
||||
def __init__(self, bar_code, name, price, stock=0, hidden=False):
|
||||
self.name = name
|
||||
self.bar_code = bar_code
|
||||
self.price = price
|
||||
self.stock = stock
|
||||
self.hidden = hidden
|
||||
|
||||
def __str__(self) -> str:
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from datetime import datetime
|
||||
import math
|
||||
|
||||
from sqlalchemy import (
|
||||
DateTime,
|
||||
Integer,
|
||||
@@ -29,24 +29,23 @@ class Purchase(Base):
|
||||
price: Mapped[int] = mapped_column(Integer)
|
||||
|
||||
transactions: Mapped[set[Transaction]] = relationship(
|
||||
back_populates="purchase",
|
||||
order_by=Transaction.user_name,
|
||||
back_populates="purchase", order_by="Transaction.user_name"
|
||||
)
|
||||
entries: Mapped[set[PurchaseEntry]] = relationship(back_populates="purchase")
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
def is_complete(self):
|
||||
return len(self.transactions) > 0 and len(self.entries) > 0
|
||||
|
||||
def price_per_transaction(self, round_up: bool = True) -> int:
|
||||
def price_per_transaction(self, round_up=True):
|
||||
if round_up:
|
||||
return int(math.ceil(float(self.price) / len(self.transactions)))
|
||||
else:
|
||||
return int(math.floor(float(self.price) / len(self.transactions)))
|
||||
|
||||
def set_price(self, round_up: bool = True) -> None:
|
||||
def set_price(self, round_up=True):
|
||||
self.price = 0
|
||||
for entry in self.entries:
|
||||
self.price += entry.amount * entry.product.price
|
||||
@@ -54,16 +53,16 @@ class Purchase(Base):
|
||||
for t in self.transactions:
|
||||
t.amount = self.price_per_transaction(round_up=round_up)
|
||||
|
||||
def perform_purchase(self, ignore_penalty: bool = False, round_up: bool = True) -> None:
|
||||
self.time = datetime.now()
|
||||
def perform_purchase(self, ignore_penalty=False, round_up=True):
|
||||
self.time = datetime.datetime.now()
|
||||
self.set_price(round_up=round_up)
|
||||
for t in self.transactions:
|
||||
t.perform_transaction(ignore_penalty=ignore_penalty)
|
||||
for entry in self.entries:
|
||||
entry.product.stock -= entry.amount
|
||||
|
||||
def perform_soft_purchase(self, price: int, round_up: bool = True) -> None:
|
||||
self.time = datetime.now()
|
||||
def perform_soft_purchase(self, price, round_up=True):
|
||||
self.time = datetime.datetime.now()
|
||||
self.price = price
|
||||
for t in self.transactions:
|
||||
t.amount = self.price_per_transaction(round_up=round_up)
|
||||
|
||||
@@ -27,8 +27,8 @@ class PurchaseEntry(Base):
|
||||
product_id: Mapped[int] = mapped_column(ForeignKey("products.product_id"))
|
||||
purchase_id: Mapped[int] = mapped_column(ForeignKey("purchases.id"))
|
||||
|
||||
product: Mapped[Product] = relationship(back_populates='purchases', lazy="joined")
|
||||
purchase: Mapped[Purchase] = relationship(back_populates='entries', lazy="joined")
|
||||
product: Mapped[Product] = relationship(lazy="joined")
|
||||
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
||||
|
||||
def __init__(self, purchase, product, amount):
|
||||
self.product = product
|
||||
|
||||
@@ -36,24 +36,17 @@ class Transaction(Base):
|
||||
purchase_id: Mapped[int | None] = mapped_column(ForeignKey("purchases.id"))
|
||||
|
||||
user: Mapped[User] = relationship(lazy="joined")
|
||||
purchase: Mapped[Purchase] = relationship(back_populates='transactions', lazy="joined")
|
||||
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user: User,
|
||||
amount: int = 0,
|
||||
description: str | None = None,
|
||||
purchase: Purchase | None = None,
|
||||
penalty: int = 1,
|
||||
):
|
||||
def __init__(self, user, amount=0, description=None, purchase=None, penalty=1):
|
||||
self.user = user
|
||||
self.amount = amount
|
||||
self.description = description
|
||||
self.purchase = purchase
|
||||
self.penalty = penalty
|
||||
|
||||
def perform_transaction(self, ignore_penalty: bool = False) -> None:
|
||||
self.time = datetime.now()
|
||||
def perform_transaction(self, ignore_penalty=False):
|
||||
self.time = datetime.datetime.now()
|
||||
if not ignore_penalty:
|
||||
self.amount *= self.penalty
|
||||
self.user.credit -= self.amount
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
@@ -15,34 +14,25 @@ from sqlalchemy.orm import (
|
||||
from .Base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .Transaction import Transaction
|
||||
from .UserProducts import UserProducts
|
||||
from .Transaction import Transaction
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
name: Mapped[str] = mapped_column(String(10), primary_key=True)
|
||||
credit: Mapped[int] = mapped_column(Integer)
|
||||
credit: Mapped[str] = mapped_column(Integer)
|
||||
card: Mapped[str | None] = mapped_column(String(20))
|
||||
rfid: Mapped[str | None] = mapped_column(String(20))
|
||||
|
||||
products: Mapped[list[UserProducts]] = relationship(back_populates="user")
|
||||
transactions: Mapped[list[Transaction]] = relationship(
|
||||
back_populates="user",
|
||||
order_by="Transaction.time",
|
||||
)
|
||||
products: Mapped[set[UserProducts]] = relationship(back_populates="user")
|
||||
transactions: Mapped[set[Transaction]] = relationship(back_populates="user")
|
||||
|
||||
name_re = r"[a-z]+"
|
||||
card_re = r"(([Nn][Tt][Nn][Uu])?[0-9]+)?"
|
||||
rfid_re = r"[0-9a-fA-F]*"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
card: str | None,
|
||||
rfid: str | None = None,
|
||||
credit: int = 0,
|
||||
):
|
||||
def __init__(self, name, card, rfid=None, credit=0):
|
||||
self.name = name
|
||||
if card == "":
|
||||
card = None
|
||||
|
||||
@@ -27,5 +27,5 @@ class UserProducts(Base):
|
||||
count: Mapped[int] = mapped_column(Integer)
|
||||
sign: Mapped[int] = mapped_column(Integer)
|
||||
|
||||
user: Mapped[User] = relationship(back_populates="products")
|
||||
product: Mapped[Product] = relationship(back_populates="users")
|
||||
user: Mapped[User] = relationship()
|
||||
product: Mapped[Product] = relationship()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
__all__ = [
|
||||
"Base",
|
||||
"Product",
|
||||
"Purchase",
|
||||
"PurchaseEntry",
|
||||
"Transaction",
|
||||
"User",
|
||||
"UserProducts",
|
||||
'Base',
|
||||
'Product',
|
||||
'Purchase',
|
||||
'PurchaseEntry',
|
||||
'Transaction',
|
||||
'User',
|
||||
'UserProducts',
|
||||
]
|
||||
|
||||
from .Base import Base
|
||||
|
||||
@@ -4,93 +4,59 @@
|
||||
import random
|
||||
import sys
|
||||
import traceback
|
||||
from signal import (
|
||||
SIG_IGN,
|
||||
SIGQUIT,
|
||||
SIGTSTP,
|
||||
)
|
||||
from signal import (
|
||||
signal as set_signal_handler,
|
||||
)
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..conf import config
|
||||
from ..menus import (
|
||||
AddProductMenu,
|
||||
AddStockMenu,
|
||||
AddUserMenu,
|
||||
AdjustCreditMenu,
|
||||
AdjustStockMenu,
|
||||
BalanceMenu,
|
||||
BuyMenu,
|
||||
CleanupStockMenu,
|
||||
EditProductMenu,
|
||||
EditUserMenu,
|
||||
FAQMenu,
|
||||
LoggedStatisticsMenu,
|
||||
MainMenu,
|
||||
Menu,
|
||||
PrintLabelMenu,
|
||||
ProductListMenu,
|
||||
ProductPopularityMenu,
|
||||
ProductRevenueMenu,
|
||||
ProductSearchMenu,
|
||||
ShowUserMenu,
|
||||
TransferMenu,
|
||||
UserListMenu,
|
||||
)
|
||||
from ..lib.helpers import *
|
||||
from ..menus import *
|
||||
|
||||
random.seed()
|
||||
|
||||
|
||||
def main(sql_session: Session):
|
||||
if not config["general"]["stop_allowed"]:
|
||||
set_signal_handler(SIGQUIT, SIG_IGN)
|
||||
def main():
|
||||
if not config.getboolean("general", "stop_allowed"):
|
||||
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
|
||||
|
||||
if not config["general"]["stop_allowed"]:
|
||||
set_signal_handler(SIGTSTP, SIG_IGN)
|
||||
if not config.getboolean("general", "stop_allowed"):
|
||||
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
|
||||
|
||||
main = MainMenu(
|
||||
sql_session,
|
||||
"Dibbler main menu",
|
||||
items=[
|
||||
BuyMenu(sql_session),
|
||||
ProductListMenu(sql_session),
|
||||
ShowUserMenu(sql_session),
|
||||
UserListMenu(sql_session),
|
||||
AdjustCreditMenu(sql_session),
|
||||
TransferMenu(sql_session),
|
||||
AddStockMenu(sql_session),
|
||||
BuyMenu(),
|
||||
ProductListMenu(),
|
||||
ShowUserMenu(),
|
||||
UserListMenu(),
|
||||
AdjustCreditMenu(),
|
||||
TransferMenu(),
|
||||
AddStockMenu(),
|
||||
Menu(
|
||||
"Add/edit",
|
||||
sql_session,
|
||||
items=[
|
||||
AddUserMenu(sql_session),
|
||||
EditUserMenu(sql_session),
|
||||
AddProductMenu(sql_session),
|
||||
EditProductMenu(sql_session),
|
||||
AdjustStockMenu(sql_session),
|
||||
CleanupStockMenu(sql_session),
|
||||
AddUserMenu(),
|
||||
EditUserMenu(),
|
||||
AddProductMenu(),
|
||||
EditProductMenu(),
|
||||
AdjustStockMenu(),
|
||||
CleanupStockMenu(),
|
||||
],
|
||||
),
|
||||
ProductSearchMenu(sql_session),
|
||||
ProductSearchMenu(),
|
||||
Menu(
|
||||
"Statistics",
|
||||
sql_session,
|
||||
items=[
|
||||
ProductPopularityMenu(sql_session),
|
||||
ProductRevenueMenu(sql_session),
|
||||
BalanceMenu(sql_session),
|
||||
LoggedStatisticsMenu(sql_session),
|
||||
ProductPopularityMenu(),
|
||||
ProductRevenueMenu(),
|
||||
BalanceMenu(),
|
||||
LoggedStatisticsMenu(),
|
||||
],
|
||||
),
|
||||
FAQMenu(sql_session),
|
||||
PrintLabelMenu(sql_session),
|
||||
FAQMenu(),
|
||||
PrintLabelMenu(),
|
||||
],
|
||||
exit_msg="happy happy joy joy",
|
||||
exit_confirm_msg="Really quit Dibbler?",
|
||||
)
|
||||
if not config["general"]["quit_allowed"]:
|
||||
if not config.getboolean("general", "quit_allowed"):
|
||||
main.exit_disallowed_msg = "You can check out any time you like, but you can never leave."
|
||||
while True:
|
||||
# noinspection PyBroadException
|
||||
@@ -102,8 +68,12 @@ def main(sql_session: Session):
|
||||
except:
|
||||
print("Something went wrong.")
|
||||
print(f"{sys.exc_info()[0]}: {sys.exc_info()[1]}")
|
||||
if config["general"]["show_tracebacks"]:
|
||||
if config.getboolean("general", "show_tracebacks"):
|
||||
traceback.print_tb(sys.exc_info()[2])
|
||||
else:
|
||||
break
|
||||
print("Restarting main menu.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
from dibbler.models import Base
|
||||
from dibbler.db import engine
|
||||
|
||||
|
||||
def main(engine: Engine):
|
||||
def main():
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import json
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models.Product import Product
|
||||
from dibbler.models.User import User
|
||||
|
||||
JSON_FILE = Path(__file__).parent.parent.parent / "mock_data.json"
|
||||
|
||||
|
||||
def clear_db(sql_session: Session):
|
||||
sql_session.query(Product).delete()
|
||||
sql_session.query(User).delete()
|
||||
sql_session.commit()
|
||||
|
||||
|
||||
def main(sql_session: Session):
|
||||
clear_db(sql_session)
|
||||
product_items = []
|
||||
user_items = []
|
||||
|
||||
with open(JSON_FILE) as f:
|
||||
json_obj = json.load(f)
|
||||
|
||||
for product in json_obj["products"]:
|
||||
product_item = Product(
|
||||
bar_code=product["bar_code"],
|
||||
name=product["name"],
|
||||
price=product["price"],
|
||||
stock=product["stock"],
|
||||
)
|
||||
product_items.append(product_item)
|
||||
|
||||
for user in json_obj["users"]:
|
||||
user_item = User(
|
||||
name=user["name"],
|
||||
card=user["card"],
|
||||
rfid=user["rfid"],
|
||||
credit=user["credit"],
|
||||
)
|
||||
user_items.append(user_item)
|
||||
|
||||
sql_session.add_all(product_items)
|
||||
sql_session.add_all(user_items)
|
||||
sql_session.commit()
|
||||
@@ -1,13 +1,18 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.db import Session
|
||||
from dibbler.models import User
|
||||
|
||||
|
||||
def main(sql_session: Session):
|
||||
def main():
|
||||
# Start an SQL session
|
||||
session = Session()
|
||||
# Let's find all users with a negative credit
|
||||
slabbedasker = sql_session.query(User).filter(User.credit < 0).all()
|
||||
slabbedasker = session.query(User).filter(User.credit < 0).all()
|
||||
|
||||
for slubbert in slabbedasker:
|
||||
print(f"{slubbert.name}, {slubbert.credit}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,231 +1,231 @@
|
||||
# #! /usr/bin/env python
|
||||
#! /usr/bin/env python
|
||||
|
||||
# # TODO: fixme
|
||||
# TODO: fixme
|
||||
|
||||
# # -*- coding: UTF-8 -*-
|
||||
# import matplotlib.pyplot as plt
|
||||
# import matplotlib.dates as mdates
|
||||
# -*- coding: UTF-8 -*-
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
|
||||
# from dibbler.lib.statistikkHelpers import *
|
||||
from dibbler.lib.statistikkHelpers import *
|
||||
|
||||
|
||||
# def getInputType():
|
||||
# inp = 0
|
||||
# while not (inp == "1" or inp == "2" or inp == "3" or inp == "4"):
|
||||
# print("type 1 for user-statistics")
|
||||
# print("type 2 for product-statistics")
|
||||
# print("type 3 for global-statistics")
|
||||
# print("type 4 to enter loop-mode")
|
||||
# inp = input("")
|
||||
# return int(inp)
|
||||
def getInputType():
|
||||
inp = 0
|
||||
while not (inp == "1" or inp == "2" or inp == "3" or inp == "4"):
|
||||
print("type 1 for user-statistics")
|
||||
print("type 2 for product-statistics")
|
||||
print("type 3 for global-statistics")
|
||||
print("type 4 to enter loop-mode")
|
||||
inp = input("")
|
||||
return int(inp)
|
||||
|
||||
|
||||
# def getDateFile(date, n):
|
||||
# try:
|
||||
# if n == 0:
|
||||
# inp = input("start date? (yyyy-mm-dd) ")
|
||||
# elif n == -1:
|
||||
# inp = input("end date? (yyyy-mm-dd) ")
|
||||
# year = inp.partition("-")
|
||||
# month = year[2].partition("-")
|
||||
# return datetime.date(int(year[0]), int(month[0]), int(month[2]))
|
||||
# except:
|
||||
# print("invalid date, setting start start date")
|
||||
# if n == 0:
|
||||
# print("to date found on first line")
|
||||
# elif n == -1:
|
||||
# print("to date found on last line")
|
||||
# print(date)
|
||||
# return datetime.date(
|
||||
# int(date.partition("-")[0]),
|
||||
# int(date.partition("-")[2].partition("-")[0]),
|
||||
# int(date.partition("-")[2].partition("-")[2]),
|
||||
# )
|
||||
def getDateFile(date, n):
|
||||
try:
|
||||
if n == 0:
|
||||
inp = input("start date? (yyyy-mm-dd) ")
|
||||
elif n == -1:
|
||||
inp = input("end date? (yyyy-mm-dd) ")
|
||||
year = inp.partition("-")
|
||||
month = year[2].partition("-")
|
||||
return datetime.date(int(year[0]), int(month[0]), int(month[2]))
|
||||
except:
|
||||
print("invalid date, setting start start date")
|
||||
if n == 0:
|
||||
print("to date found on first line")
|
||||
elif n == -1:
|
||||
print("to date found on last line")
|
||||
print(date)
|
||||
return datetime.date(
|
||||
int(date.partition("-")[0]),
|
||||
int(date.partition("-")[2].partition("-")[0]),
|
||||
int(date.partition("-")[2].partition("-")[2]),
|
||||
)
|
||||
|
||||
|
||||
# def dateToDateNumFile(date, startDate):
|
||||
# year = date.partition("-")
|
||||
# month = year[2].partition("-")
|
||||
# day = datetime.date(int(year[0]), int(month[0]), int(month[2]))
|
||||
# deltaDays = day - startDate
|
||||
# return int(deltaDays.days), day.weekday()
|
||||
def dateToDateNumFile(date, startDate):
|
||||
year = date.partition("-")
|
||||
month = year[2].partition("-")
|
||||
day = datetime.date(int(year[0]), int(month[0]), int(month[2]))
|
||||
deltaDays = day - startDate
|
||||
return int(deltaDays.days), day.weekday()
|
||||
|
||||
|
||||
# def getProducts(products):
|
||||
# product = []
|
||||
# products = products.partition("¤")
|
||||
# product.append(products[0])
|
||||
# while products[1] == "¤":
|
||||
# products = products[2].partition("¤")
|
||||
# product.append(products[0])
|
||||
# return product
|
||||
def getProducts(products):
|
||||
product = []
|
||||
products = products.partition("¤")
|
||||
product.append(products[0])
|
||||
while products[1] == "¤":
|
||||
products = products[2].partition("¤")
|
||||
product.append(products[0])
|
||||
return product
|
||||
|
||||
|
||||
# def piePlot(dictionary, n):
|
||||
# keys = []
|
||||
# values = []
|
||||
# i = 0
|
||||
# for key in sorted(dictionary, key=dictionary.get, reverse=True):
|
||||
# values.append(dictionary[key])
|
||||
# if i < n:
|
||||
# keys.append(key)
|
||||
# i += 1
|
||||
# else:
|
||||
# keys.append("")
|
||||
# plt.pie(values, labels=keys)
|
||||
def piePlot(dictionary, n):
|
||||
keys = []
|
||||
values = []
|
||||
i = 0
|
||||
for key in sorted(dictionary, key=dictionary.get, reverse=True):
|
||||
values.append(dictionary[key])
|
||||
if i < n:
|
||||
keys.append(key)
|
||||
i += 1
|
||||
else:
|
||||
keys.append("")
|
||||
plt.pie(values, labels=keys)
|
||||
|
||||
|
||||
# def datePlot(array, dateLine):
|
||||
# if not array == []:
|
||||
# plt.bar(dateLine, array)
|
||||
# plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%b"))
|
||||
def datePlot(array, dateLine):
|
||||
if not array == []:
|
||||
plt.bar(dateLine, array)
|
||||
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%b"))
|
||||
|
||||
|
||||
# def dayPlot(array, days):
|
||||
# if not array == []:
|
||||
# for i in range(7):
|
||||
# array[i] = array[i] * 7.0 / days
|
||||
# plt.bar(list(range(7)), array)
|
||||
# plt.xticks(
|
||||
# list(range(7)),
|
||||
# [
|
||||
# " mon",
|
||||
# " tue",
|
||||
# " wed",
|
||||
# " thu",
|
||||
# " fri",
|
||||
# " sat",
|
||||
# " sun",
|
||||
# ],
|
||||
# )
|
||||
def dayPlot(array, days):
|
||||
if not array == []:
|
||||
for i in range(7):
|
||||
array[i] = array[i] * 7.0 / days
|
||||
plt.bar(list(range(7)), array)
|
||||
plt.xticks(
|
||||
list(range(7)),
|
||||
[
|
||||
" mon",
|
||||
" tue",
|
||||
" wed",
|
||||
" thu",
|
||||
" fri",
|
||||
" sat",
|
||||
" sun",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# def graphPlot(array, dateLine):
|
||||
# if not array == []:
|
||||
# plt.plot(dateLine, array)
|
||||
# plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%b"))
|
||||
def graphPlot(array, dateLine):
|
||||
if not array == []:
|
||||
plt.plot(dateLine, array)
|
||||
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%b"))
|
||||
|
||||
|
||||
# def plotUser(database, dateLine, user, n):
|
||||
# printUser(database, dateLine, user, n)
|
||||
# plt.subplot(221)
|
||||
# piePlot(database.personVareAntall[user], n)
|
||||
# plt.xlabel("antall varer kjøpt gjengitt i antall")
|
||||
# plt.subplot(222)
|
||||
# datePlot(database.personDatoVerdi[user], dateLine)
|
||||
# plt.xlabel("penger brukt over dato")
|
||||
# plt.subplot(223)
|
||||
# piePlot(database.personVareVerdi[user], n)
|
||||
# plt.xlabel("antall varer kjøpt gjengitt i verdi")
|
||||
# plt.subplot(224)
|
||||
# dayPlot(database.personUkedagVerdi[user], len(dateLine))
|
||||
# plt.xlabel("forbruk over ukedager")
|
||||
# plt.show()
|
||||
def plotUser(database, dateLine, user, n):
|
||||
printUser(database, dateLine, user, n)
|
||||
plt.subplot(221)
|
||||
piePlot(database.personVareAntall[user], n)
|
||||
plt.xlabel("antall varer kjøpt gjengitt i antall")
|
||||
plt.subplot(222)
|
||||
datePlot(database.personDatoVerdi[user], dateLine)
|
||||
plt.xlabel("penger brukt over dato")
|
||||
plt.subplot(223)
|
||||
piePlot(database.personVareVerdi[user], n)
|
||||
plt.xlabel("antall varer kjøpt gjengitt i verdi")
|
||||
plt.subplot(224)
|
||||
dayPlot(database.personUkedagVerdi[user], len(dateLine))
|
||||
plt.xlabel("forbruk over ukedager")
|
||||
plt.show()
|
||||
|
||||
|
||||
# def plotProduct(database, dateLine, product, n):
|
||||
# printProduct(database, dateLine, product, n)
|
||||
# plt.subplot(221)
|
||||
# piePlot(database.varePersonAntall[product], n)
|
||||
# plt.xlabel("personer som har handler produktet")
|
||||
# plt.subplot(222)
|
||||
# datePlot(database.vareDatoAntall[product], dateLine)
|
||||
# plt.xlabel("antall produkter handlet per dag")
|
||||
# # plt.subplot(223)
|
||||
# plt.subplot(224)
|
||||
# dayPlot(database.vareUkedagAntall[product], len(dateLine))
|
||||
# plt.xlabel("antall over ukedager")
|
||||
# plt.show()
|
||||
def plotProduct(database, dateLine, product, n):
|
||||
printProduct(database, dateLine, product, n)
|
||||
plt.subplot(221)
|
||||
piePlot(database.varePersonAntall[product], n)
|
||||
plt.xlabel("personer som har handler produktet")
|
||||
plt.subplot(222)
|
||||
datePlot(database.vareDatoAntall[product], dateLine)
|
||||
plt.xlabel("antall produkter handlet per dag")
|
||||
# plt.subplot(223)
|
||||
plt.subplot(224)
|
||||
dayPlot(database.vareUkedagAntall[product], len(dateLine))
|
||||
plt.xlabel("antall over ukedager")
|
||||
plt.show()
|
||||
|
||||
|
||||
# def plotGlobal(database, dateLine, n):
|
||||
# printGlobal(database, dateLine, n)
|
||||
# plt.subplot(231)
|
||||
# piePlot(database.globalVareVerdi, n)
|
||||
# plt.xlabel("varer kjøpt gjengitt som verdi")
|
||||
# plt.subplot(232)
|
||||
# datePlot(database.globalDatoForbruk, dateLine)
|
||||
# plt.xlabel("forbruk over dato")
|
||||
# plt.subplot(233)
|
||||
# graphPlot(database.pengebeholdning, dateLine)
|
||||
# plt.xlabel("pengebeholdning over tid (negativ verdi utgjør samlet kreditt)")
|
||||
# plt.subplot(234)
|
||||
# piePlot(database.globalPersonForbruk, n)
|
||||
# plt.xlabel("penger brukt av personer")
|
||||
# plt.subplot(235)
|
||||
# dayPlot(database.globalUkedagForbruk, len(dateLine))
|
||||
# plt.xlabel("forbruk over ukedager")
|
||||
# plt.show()
|
||||
def plotGlobal(database, dateLine, n):
|
||||
printGlobal(database, dateLine, n)
|
||||
plt.subplot(231)
|
||||
piePlot(database.globalVareVerdi, n)
|
||||
plt.xlabel("varer kjøpt gjengitt som verdi")
|
||||
plt.subplot(232)
|
||||
datePlot(database.globalDatoForbruk, dateLine)
|
||||
plt.xlabel("forbruk over dato")
|
||||
plt.subplot(233)
|
||||
graphPlot(database.pengebeholdning, dateLine)
|
||||
plt.xlabel("pengebeholdning over tid (negativ verdi utgjør samlet kreditt)")
|
||||
plt.subplot(234)
|
||||
piePlot(database.globalPersonForbruk, n)
|
||||
plt.xlabel("penger brukt av personer")
|
||||
plt.subplot(235)
|
||||
dayPlot(database.globalUkedagForbruk, len(dateLine))
|
||||
plt.xlabel("forbruk over ukedager")
|
||||
plt.show()
|
||||
|
||||
|
||||
# def alt4menu(database, dateLine, useDatabase):
|
||||
# n = 10
|
||||
# while 1:
|
||||
# print(
|
||||
# "\n1: user-statistics, 2: product-statistics, 3:global-statistics, n: adjust amount of data shown q:quit"
|
||||
# )
|
||||
# try:
|
||||
# inp = input("")
|
||||
# except:
|
||||
# continue
|
||||
# if inp == "q":
|
||||
# break
|
||||
# elif inp == "1":
|
||||
# if i == "0":
|
||||
# user = input("input full username: ")
|
||||
# else:
|
||||
# user = getUser()
|
||||
# plotUser(database, dateLine, user, n)
|
||||
# elif inp == "2":
|
||||
# if i == "0":
|
||||
# product = input("input full product name: ")
|
||||
# else:
|
||||
# product = getProduct()
|
||||
# plotProduct(database, dateLine, product, n)
|
||||
# elif inp == "3":
|
||||
# plotGlobal(database, dateLine, n)
|
||||
# elif inp == "n":
|
||||
# try:
|
||||
# n = int(input("set number to show "))
|
||||
# except:
|
||||
# pass
|
||||
def alt4menu(database, dateLine, useDatabase):
|
||||
n = 10
|
||||
while 1:
|
||||
print(
|
||||
"\n1: user-statistics, 2: product-statistics, 3:global-statistics, n: adjust amount of data shown q:quit"
|
||||
)
|
||||
try:
|
||||
inp = input("")
|
||||
except:
|
||||
continue
|
||||
if inp == "q":
|
||||
break
|
||||
elif inp == "1":
|
||||
if i == "0":
|
||||
user = input("input full username: ")
|
||||
else:
|
||||
user = getUser()
|
||||
plotUser(database, dateLine, user, n)
|
||||
elif inp == "2":
|
||||
if i == "0":
|
||||
product = input("input full product name: ")
|
||||
else:
|
||||
product = getProduct()
|
||||
plotProduct(database, dateLine, product, n)
|
||||
elif inp == "3":
|
||||
plotGlobal(database, dateLine, n)
|
||||
elif inp == "n":
|
||||
try:
|
||||
n = int(input("set number to show "))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# def main():
|
||||
# inputType = getInputType()
|
||||
# i = input("0:fil, 1:database \n? ")
|
||||
# if inputType == 1:
|
||||
# if i == "0":
|
||||
# user = input("input full username: ")
|
||||
# else:
|
||||
# user = getUser()
|
||||
# product = ""
|
||||
# elif inputType == 2:
|
||||
# if i == "0":
|
||||
# product = input("input full product name: ")
|
||||
# else:
|
||||
# product = getProduct()
|
||||
# user = ""
|
||||
# else:
|
||||
# product = ""
|
||||
# user = ""
|
||||
# if i == "0":
|
||||
# inputFile = input("logfil? ")
|
||||
# if inputFile == "":
|
||||
# inputFile = "default.dibblerlog"
|
||||
# database, dateLine = buildDatabaseFromFile(inputFile, inputType, product, user)
|
||||
# else:
|
||||
# database, dateLine = buildDatabaseFromDb(inputType, product, user)
|
||||
def main():
|
||||
inputType = getInputType()
|
||||
i = input("0:fil, 1:database \n? ")
|
||||
if inputType == 1:
|
||||
if i == "0":
|
||||
user = input("input full username: ")
|
||||
else:
|
||||
user = getUser()
|
||||
product = ""
|
||||
elif inputType == 2:
|
||||
if i == "0":
|
||||
product = input("input full product name: ")
|
||||
else:
|
||||
product = getProduct()
|
||||
user = ""
|
||||
else:
|
||||
product = ""
|
||||
user = ""
|
||||
if i == "0":
|
||||
inputFile = input("logfil? ")
|
||||
if inputFile == "":
|
||||
inputFile = "default.dibblerlog"
|
||||
database, dateLine = buildDatabaseFromFile(inputFile, inputType, product, user)
|
||||
else:
|
||||
database, dateLine = buildDatabaseFromDb(inputType, product, user)
|
||||
|
||||
# if inputType == 1:
|
||||
# plotUser(database, dateLine, user, 10)
|
||||
# if inputType == 2:
|
||||
# plotProduct(database, dateLine, product, 10)
|
||||
# if inputType == 3:
|
||||
# plotGlobal(database, dateLine, 10)
|
||||
# if inputType == 4:
|
||||
# alt4menu(database, dateLine, i)
|
||||
if inputType == 1:
|
||||
plotUser(database, dateLine, user, 10)
|
||||
if inputType == 2:
|
||||
plotProduct(database, dateLine, product, 10)
|
||||
if inputType == 3:
|
||||
plotGlobal(database, dateLine, 10)
|
||||
if inputType == 4:
|
||||
alt4menu(database, dateLine, i)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# main()
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
21
example-config.ini
Normal file
21
example-config.ini
Normal file
@@ -0,0 +1,21 @@
|
||||
[general]
|
||||
; quit_allowed = false
|
||||
; stop_allowed = false
|
||||
quit_allowed = true ; not for prod
|
||||
stop_allowed = true ; not for prod
|
||||
show_tracebacks = true
|
||||
input_encoding = 'utf8'
|
||||
|
||||
[database]
|
||||
; url = postgresql://dibbler:hunter2@127.0.0.1/pvvvv
|
||||
url = sqlite:///test.db ; devenv will override this to postgres using DIBBLER_DATABASE_URL
|
||||
|
||||
[limits]
|
||||
low_credit_warning_limit = -100
|
||||
user_recent_transaction_limit = 100
|
||||
|
||||
# See https://pypi.org/project/brother_ql_next/ for label types
|
||||
# Set rotate to False for endless labels
|
||||
[printer]
|
||||
label_type = "62"
|
||||
label_rotate = false
|
||||
@@ -1,35 +0,0 @@
|
||||
[general]
|
||||
quit_allowed = true
|
||||
stop_allowed = false
|
||||
show_tracebacks = true
|
||||
input_encoding = 'utf8'
|
||||
|
||||
[database]
|
||||
type = 'sqlite'
|
||||
|
||||
[database.sqlite]
|
||||
path = 'sqlite:///test.db'
|
||||
|
||||
[database.postgresql]
|
||||
host = 'localhost'
|
||||
# host = '/run/postgresql'
|
||||
port = 5432
|
||||
|
||||
username = 'dibbler'
|
||||
dbname = 'dibbler'
|
||||
|
||||
# You can either specify a path to a file containing the password,
|
||||
# or just specify the password directly
|
||||
# password = 'superhemlig'
|
||||
# password_file = '/var/lib/dibbler/db-password'
|
||||
|
||||
|
||||
[limits]
|
||||
low_credit_warning_limit = -100
|
||||
user_recent_transaction_limit = 100
|
||||
|
||||
# See https://pypi.org/project/brother_ql/ for label types
|
||||
# Set rotate to False for endless labels
|
||||
[printer]
|
||||
label_type = '62'
|
||||
label_rotate = false
|
||||
278
flake.lock
generated
278
flake.lock
generated
@@ -1,12 +1,217 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cachix": {
|
||||
"inputs": {
|
||||
"devenv": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"git-hooks": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1728672398,
|
||||
"narHash": "sha256-KxuGSoVUFnQLB2ZcYODW7AVPAh9JqRlD5BrfsC/Q4qs=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "aac51f698309fd0f381149214b7eee213c66ef0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "latest",
|
||||
"repo": "cachix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv": {
|
||||
"inputs": {
|
||||
"cachix": "cachix",
|
||||
"flake-compat": "flake-compat",
|
||||
"git-hooks": "git-hooks",
|
||||
"nix": "nix",
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731619804,
|
||||
"narHash": "sha256-wyxFaVooL8SzvQNpolpx32X+GoBPnCAg9E0i/Ekn3FU=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "87edaaf1dddf17fe16eabab3c8edaf7cca2c3bc2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"devenv",
|
||||
"nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1712014858,
|
||||
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1730302582,
|
||||
"narHash": "sha256-W1MIJpADXQCgosJZT8qBYLRuZls2KSiKdpnTVdKBuvU=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"libgit2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1697646580,
|
||||
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
|
||||
"owner": "libgit2",
|
||||
"repo": "libgit2",
|
||||
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "libgit2",
|
||||
"repo": "libgit2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-parts": "flake-parts",
|
||||
"libgit2": "libgit2",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs-23-11": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs-regression": [
|
||||
"devenv"
|
||||
],
|
||||
"pre-commit-hooks": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1727438425,
|
||||
"narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=",
|
||||
"owner": "domenkozar",
|
||||
"repo": "nix",
|
||||
"rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "domenkozar",
|
||||
"ref": "devenv-2.24",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1764950072,
|
||||
"narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
|
||||
"lastModified": 1730531603,
|
||||
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f61125a668a320878494449750330ca58b78c557",
|
||||
"rev": "7ffd9ae656aec493492b44d0ddfb28e79a1ea25d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -16,9 +221,74 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1717432640,
|
||||
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "release-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1716977621,
|
||||
"narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "rolling",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1731611831,
|
||||
"narHash": "sha256-R51rOqkWMfubBkZ9BY4Y1VaRoeqEBshlfQ8mMH5RjqI=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cea28c811faadb50bee00d433bbf2fea845a43e4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable-small",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
"devenv": "devenv",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
172
flake.nix
172
flake.nix
@@ -1,72 +1,128 @@
|
||||
{
|
||||
description = "Dibbler samspleisebod";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable-small";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
devenv.url = "github:cachix/devenv";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }: let
|
||||
inherit (nixpkgs) lib;
|
||||
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
nixConfig = {
|
||||
extra-trusted-public-keys = [
|
||||
"devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="
|
||||
];
|
||||
extra-substituters = [
|
||||
"https://devenv.cachix.org"
|
||||
];
|
||||
};
|
||||
|
||||
forAllSystems = f: lib.genAttrs systems (system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in f system pkgs);
|
||||
in {
|
||||
apps = let
|
||||
mkApp = program: description: {
|
||||
type = "app";
|
||||
program = toString program;
|
||||
meta = {
|
||||
inherit description;
|
||||
};
|
||||
};
|
||||
mkVm = name: mkApp "${self.nixosConfigurations.${name}.config.system.build.vm}/bin/run-nixos-vm";
|
||||
in forAllSystems (system: pkgs: {
|
||||
default = self.apps.${system}.dibbler;
|
||||
dibbler = let
|
||||
app = pkgs.writeShellApplication {
|
||||
name = "dibbler-with-default-config";
|
||||
runtimeInputs = [ self.packages.${system}.dibbler ];
|
||||
text = ''
|
||||
dibbler -c ${./example-config.toml} "$@"
|
||||
outputs = { self, ... } @ inputs:
|
||||
inputs.flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||
inherit (pkgs) lib;
|
||||
in {
|
||||
|
||||
packages = {
|
||||
default = self.packages.${system}.dibbler;
|
||||
|
||||
dibbler = pkgs.python311Packages.callPackage ./nix/dibbler.nix { };
|
||||
skrot-vm = self.nixosConfigurations.skrot.config.system.build.vm;
|
||||
|
||||
# devenv cruft
|
||||
devenv-up = self.devShells.${system}.default.config.procfileScript;
|
||||
devenv-test = self.devShells.${system}.default.config.test;
|
||||
};
|
||||
|
||||
devShells = {
|
||||
default = self.devShells.${system}.dibbler;
|
||||
dibbler = inputs.devenv.lib.mkShell {
|
||||
inherit inputs pkgs;
|
||||
modules = [({ config, ... }: {
|
||||
# https://devenv.sh/reference/options/
|
||||
|
||||
enterShell = ''
|
||||
if [[ ! -f config.ini ]]; then
|
||||
cp -v example-config.ini config.ini
|
||||
fi
|
||||
|
||||
export REPO_ROOT=$(realpath .) # used by mkPythonEditablePackage
|
||||
export DIBBLER_CONFIG_FILE=$(realpath config.ini)
|
||||
export DIBBLER_DATABASE_URL=postgresql://dibbler:hunter2@/dibbler?host=${config.env.PGHOST}
|
||||
'';
|
||||
};
|
||||
in mkApp (lib.getExe app) "Run the dibbler cli with its default config against an SQLite database";
|
||||
vm = mkVm "vm" "Start a NixOS VM with dibbler installed in kiosk-mode";
|
||||
vm-non-kiosk = mkVm "vm-non-kiosk" "Start a NixOS VM with dibbler installed in nonkiosk-mode";
|
||||
});
|
||||
|
||||
packages = [
|
||||
|
||||
/* self.packages.${system}.dibbler */
|
||||
(pkgs.python311Packages.mkPythonEditablePackage {
|
||||
inherit (self.packages.${system}.dibbler)
|
||||
pname version
|
||||
build-system dependencies;
|
||||
scripts = (lib.importTOML ./pyproject.toml).project.scripts;
|
||||
root = "$REPO_ROOT";
|
||||
})
|
||||
|
||||
pkgs.python311Packages.black
|
||||
pkgs.ruff
|
||||
];
|
||||
|
||||
services.postgres = {
|
||||
enable = true;
|
||||
initialDatabases = [
|
||||
{
|
||||
name = "dibbler";
|
||||
user = "dibbler";
|
||||
pass = "hunter2";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
})];
|
||||
};
|
||||
};
|
||||
|
||||
})
|
||||
|
||||
//
|
||||
|
||||
{
|
||||
# Note: using the module requires that you have applied the
|
||||
# overlay first
|
||||
nixosModules.default = import ./nix/module.nix;
|
||||
|
||||
nixosConfigurations = {
|
||||
vm = import ./nix/nixos-configurations/vm.nix { inherit self nixpkgs; };
|
||||
vm-non-kiosk = import ./nix/nixos-configurations/vm-non-kiosk.nix { inherit self nixpkgs; };
|
||||
images.skrot = self.nixosConfigurations.skrot.config.system.build.sdImage;
|
||||
|
||||
nixosConfigurations.skrot = inputs.nixpkgs.lib.nixosSystem {
|
||||
system = "aarch64-linux";
|
||||
modules = [
|
||||
(inputs.nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix")
|
||||
self.nixosModules.default
|
||||
({...}: {
|
||||
system.stateVersion = "22.05";
|
||||
|
||||
networking = {
|
||||
hostName = "skrot";
|
||||
domain = "pvv.ntnu.no";
|
||||
nameservers = [ "129.241.0.200" "129.241.0.201" ];
|
||||
defaultGateway = "129.241.210.129";
|
||||
interfaces.eth0 = {
|
||||
useDHCP = false;
|
||||
ipv4.addresses = [{
|
||||
address = "129.241.210.235";
|
||||
prefixLength = 25;
|
||||
}];
|
||||
};
|
||||
};
|
||||
# services.resolved.enable = true;
|
||||
# systemd.network.enable = true;
|
||||
# systemd.network.networks."30-network" = {
|
||||
# matchConfig.Name = "*";
|
||||
# DHCP = "no";
|
||||
# address = [ "129.241.210.235/25" ];
|
||||
# gateway = [ "129.241.210.129" ];
|
||||
# };
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
overlays = {
|
||||
default = self.overlays.dibbler;
|
||||
dibbler = final: prev: {
|
||||
inherit (self.packages.${prev.stdenv.hostPlatform.system}) dibbler;
|
||||
};
|
||||
};
|
||||
|
||||
devShells = forAllSystems (system: pkgs: {
|
||||
default = self.devShells.${system}.dibbler;
|
||||
dibbler = pkgs.callPackage ./nix/shell.nix {
|
||||
python = pkgs.python313;
|
||||
};
|
||||
});
|
||||
|
||||
packages = forAllSystems (system: pkgs: {
|
||||
default = self.packages.${system}.dibbler;
|
||||
dibbler = pkgs.callPackage ./nix/package.nix {
|
||||
python3Packages = pkgs.python313Packages;
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"products": [
|
||||
{
|
||||
"product_id": 1,
|
||||
"bar_code": "1234567890123",
|
||||
"name": "Wireless Mouse",
|
||||
"price": 2999,
|
||||
"stock": 150,
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"product_id": 2,
|
||||
"bar_code": "9876543210987",
|
||||
"name": "Mechanical Keyboard",
|
||||
"price": 5999,
|
||||
"stock": 75,
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"product_id": 3,
|
||||
"bar_code": "1112223334445",
|
||||
"name": "Gaming Monitor",
|
||||
"price": 19999,
|
||||
"stock": 20,
|
||||
"hidden": false
|
||||
},
|
||||
{
|
||||
"product_id": 4,
|
||||
"bar_code": "5556667778889",
|
||||
"name": "USB-C Docking Station",
|
||||
"price": 8999,
|
||||
"stock": 50,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"product_id": 5,
|
||||
"bar_code": "4445556667771",
|
||||
"name": "Noise Cancelling Headphones",
|
||||
"price": 12999,
|
||||
"stock": 30,
|
||||
"hidden": true
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"name": "Albert",
|
||||
"credit": 42069,
|
||||
"card": "NTU12345678",
|
||||
"rfid": "a1b2c3d4e5"
|
||||
},
|
||||
{
|
||||
"name": "lorem",
|
||||
"credit": 2000,
|
||||
"card": "9876543210",
|
||||
"rfid": "f6e7d8c9b0"
|
||||
},
|
||||
{
|
||||
"name": "ibsum",
|
||||
"credit": 1000,
|
||||
"card": "11122233",
|
||||
"rfid": ""
|
||||
},
|
||||
{
|
||||
"name": "dave",
|
||||
"credit": 7500,
|
||||
"card": "NTU56789012",
|
||||
"rfid": "1234abcd5678"
|
||||
},
|
||||
{
|
||||
"name": "eve",
|
||||
"credit": 3000,
|
||||
"card": null,
|
||||
"rfid": "deadbeef1234"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
nix/dibbler.nix
Normal file
28
nix/dibbler.nix
Normal file
@@ -0,0 +1,28 @@
|
||||
{ lib
|
||||
, fetchFromGitHub
|
||||
, buildPythonApplication
|
||||
, setuptools
|
||||
, brother-ql
|
||||
, matplotlib
|
||||
, psycopg2
|
||||
, python-barcode
|
||||
, sqlalchemy
|
||||
}:
|
||||
|
||||
buildPythonApplication {
|
||||
pname = "dibbler";
|
||||
version = "0.0.0";
|
||||
pyproject = true;
|
||||
|
||||
src = lib.cleanSource ../.;
|
||||
|
||||
build-system = [ setuptools ];
|
||||
dependencies = [
|
||||
# we override pname to satisfy mkPythonEditablePackage
|
||||
(brother-ql.overridePythonAttrs { pname = "brother-ql-next"; })
|
||||
matplotlib
|
||||
psycopg2
|
||||
python-barcode
|
||||
sqlalchemy
|
||||
];
|
||||
}
|
||||
241
nix/module.nix
241
nix/module.nix
@@ -1,186 +1,87 @@
|
||||
{ config, pkgs, lib, ... }: let
|
||||
cfg = config.services.dibbler;
|
||||
|
||||
format = pkgs.formats.toml { };
|
||||
in {
|
||||
options.services.dibbler = {
|
||||
enable = lib.mkEnableOption "dibbler, the little kiosk computer";
|
||||
|
||||
package = lib.mkPackageOption pkgs "dibbler" { };
|
||||
|
||||
screenPackage = lib.mkPackageOption pkgs "screen" { };
|
||||
|
||||
createLocalDatabase = lib.mkEnableOption "" // {
|
||||
description = ''
|
||||
Whether to set up a local postgres database automatically.
|
||||
|
||||
::: {.note}
|
||||
You must set up postgres manually before enabling this option.
|
||||
:::
|
||||
'';
|
||||
};
|
||||
|
||||
kioskMode = lib.mkEnableOption "" // {
|
||||
description = ''
|
||||
Whether to let dibbler take over the entire machine.
|
||||
|
||||
This will restrict the machine to a single TTY and make the program unquittable.
|
||||
You can still get access to PTYs via SSH and similar, if enabled.
|
||||
'';
|
||||
};
|
||||
|
||||
limitScreenHeight = lib.mkOption {
|
||||
type = with lib.types; nullOr ints.unsigned;
|
||||
default = null;
|
||||
example = 42;
|
||||
description = ''
|
||||
If set, limits the height of the screen dibbler uses to the given number of lines.
|
||||
'';
|
||||
};
|
||||
|
||||
limitScreenWidth = lib.mkOption {
|
||||
type = with lib.types; nullOr ints.unsigned;
|
||||
default = null;
|
||||
example = 80;
|
||||
description = ''
|
||||
If set, limits the width of the screen dibbler uses to the given number of columns.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
description = "Configuration for dibbler";
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
freeformType = format.type;
|
||||
};
|
||||
config = lib.mkOption {
|
||||
default = ../conf.py;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
services.dibbler.settings = lib.pipe ../example-config.toml [
|
||||
builtins.readFile
|
||||
builtins.fromTOML
|
||||
(lib.mapAttrsRecursive (_: lib.mkDefault))
|
||||
];
|
||||
}
|
||||
{
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
config = let
|
||||
screen = "${pkgs.screen}/bin/screen";
|
||||
in {
|
||||
boot = {
|
||||
consoleLogLevel = 0;
|
||||
enableContainers = false;
|
||||
loader.grub.enable = false;
|
||||
};
|
||||
|
||||
environment.etc."dibbler/dibbler.toml".source = format.generate "dibbler.toml" cfg.settings;
|
||||
|
||||
users = {
|
||||
users.dibbler = {
|
||||
group = "dibbler";
|
||||
isNormalUser = true;
|
||||
};
|
||||
groups.dibbler = { };
|
||||
};
|
||||
|
||||
services.dibbler.settings.database = lib.mkIf cfg.createLocalDatabase {
|
||||
type = "postgresql";
|
||||
postgresql.host = "/run/postgresql";
|
||||
};
|
||||
|
||||
services.postgresql = lib.mkIf cfg.createLocalDatabase {
|
||||
ensureDatabases = [ "dibbler" ];
|
||||
ensureUsers = [{
|
||||
name = "dibbler";
|
||||
ensureDBOwnership = true;
|
||||
ensureClauses.login = true;
|
||||
}];
|
||||
};
|
||||
|
||||
systemd.services.dibbler-setup-database = lib.mkIf cfg.createLocalDatabase {
|
||||
description = "Dibbler database setup";
|
||||
wantedBy = [ "default.target" ];
|
||||
after = [ "postgresql.service" ];
|
||||
unitConfig = {
|
||||
ConditionPathExists = "!/var/lib/dibbler/.db-setup-done";
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${lib.getExe cfg.package} --config /etc/dibbler/dibbler.toml create-db";
|
||||
ExecStartPost = "${lib.getExe' pkgs.coreutils "touch"} /var/lib/dibbler/.db-setup-done";
|
||||
StateDirectory = "dibbler";
|
||||
|
||||
User = "dibbler";
|
||||
Group = "dibbler";
|
||||
};
|
||||
};
|
||||
}
|
||||
(lib.mkIf cfg.kioskMode {
|
||||
boot.kernelParams = [
|
||||
"console=tty1"
|
||||
];
|
||||
|
||||
|
||||
users.users.dibbler = {
|
||||
users = {
|
||||
groups.dibbler = { };
|
||||
users.dibbler = {
|
||||
group = "dibbler";
|
||||
extraGroups = [ "lp" ];
|
||||
shell = (pkgs.writeShellScriptBin "login-shell" "${lib.getExe' cfg.screenPackage "screen"} -x dibbler") // {
|
||||
shellPath = "/bin/login-shell";
|
||||
};
|
||||
isNormalUser = true;
|
||||
shell = (
|
||||
(pkgs.writeShellScriptBin "login-shell" "${screen} -x dibbler")
|
||||
// {shellPath = "/bin/login-shell";}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.screen-daemon = {
|
||||
description = "Dibbler service screen";
|
||||
wantedBy = [ "default.target" ];
|
||||
serviceConfig = {
|
||||
ExecStartPre = "-${screen} -X -S dibbler kill";
|
||||
ExecStart = "${screen} -dmS dibbler -O -l ${cfg.package}/bin/dibbler --config ${cfg.config} loop";
|
||||
ExecStartPost = "${screen} -X -S dibbler width 42 80";
|
||||
User = "dibbler";
|
||||
Group = "dibbler";
|
||||
Type = "forking";
|
||||
RemainAfterExit = false;
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
SuccessExitStatus = 1;
|
||||
};
|
||||
};
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/issues/84105
|
||||
boot.kernelParams = [
|
||||
"console=ttyUSB0,9600"
|
||||
"console=tty1"
|
||||
];
|
||||
systemd.services."serial-getty@ttyUSB0" = {
|
||||
enable = true;
|
||||
wantedBy = [ "getty.target" ]; # to start at boot
|
||||
serviceConfig.Restart = "always"; # restart when session is closed
|
||||
};
|
||||
|
||||
services = {
|
||||
openssh = {
|
||||
enable = true;
|
||||
permitRootLogin = "yes";
|
||||
};
|
||||
|
||||
services.dibbler.settings.general = {
|
||||
quit_allowed = false;
|
||||
stop_allowed = false;
|
||||
};
|
||||
getty.autologinUser = lib.mkForce "dibbler";
|
||||
udisks2.enable = false;
|
||||
};
|
||||
|
||||
systemd.services.dibbler-screen-session = {
|
||||
description = "Dibbler Screen Session";
|
||||
wantedBy = [
|
||||
"default.target"
|
||||
];
|
||||
after = if cfg.createLocalDatabase then [
|
||||
"postgresql.service"
|
||||
"dibbler-setup-database.service"
|
||||
] else [
|
||||
"network.target"
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "forking";
|
||||
RemainAfterExit = false;
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
SuccessExitStatus = 1;
|
||||
networking.firewall.logRefusedConnections = false;
|
||||
console.keyMap = "no";
|
||||
programs.command-not-found.enable = false;
|
||||
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
|
||||
environment.noXlibs = true;
|
||||
|
||||
User = "dibbler";
|
||||
Group = "dibbler";
|
||||
documentation = {
|
||||
info.enable = false;
|
||||
man.enable = false;
|
||||
};
|
||||
|
||||
ExecStartPre = "-${lib.getExe' cfg.screenPackage "screen"} -X -S dibbler kill";
|
||||
ExecStart = let
|
||||
screenArgs = lib.escapeShellArgs [
|
||||
# -dm creates the screen in detached mode without accessing it
|
||||
"-dm"
|
||||
|
||||
# Session name
|
||||
"-S"
|
||||
"dibbler"
|
||||
|
||||
# Set optimal output mode instead of VT100 emulation
|
||||
"-O"
|
||||
|
||||
# Enable login mode, updates utmp entries
|
||||
"-l"
|
||||
];
|
||||
|
||||
dibblerArgs = lib.cli.toCommandLineShellGNU { } {
|
||||
config = "/etc/dibbler/dibbler.toml";
|
||||
};
|
||||
|
||||
in "${lib.getExe' cfg.screenPackage "screen"} ${screenArgs} ${lib.getExe cfg.package} ${dibblerArgs} loop";
|
||||
ExecStartPost =
|
||||
lib.optionals (cfg.limitScreenWidth != null) [
|
||||
"${lib.getExe' cfg.screenPackage "screen"} -X -S dibbler width ${toString cfg.limitScreenWidth}"
|
||||
]
|
||||
++ lib.optionals (cfg.limitScreenHeight != null) [
|
||||
"${lib.getExe' cfg.screenPackage "screen"} -X -S dibbler height ${toString cfg.limitScreenHeight}"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
services.getty.autologinUser = "dibbler";
|
||||
})
|
||||
]);
|
||||
security = {
|
||||
polkit.enable = lib.mkForce false;
|
||||
audit.enable = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
{ self, nixpkgs, ... }:
|
||||
nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs {
|
||||
system = "x86_64-linux";
|
||||
overlays = [
|
||||
self.overlays.dibbler
|
||||
];
|
||||
};
|
||||
modules = [
|
||||
"${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix"
|
||||
"${nixpkgs}/nixos/tests/common/user-account.nix"
|
||||
|
||||
self.nixosModules.default
|
||||
|
||||
({ config, ... }: {
|
||||
system.stateVersion = config.system.nixos.release;
|
||||
virtualisation.graphics = false;
|
||||
|
||||
users.motd = ''
|
||||
=================================
|
||||
Welcome to the dibbler non-kiosk vm!
|
||||
|
||||
Try running:
|
||||
${config.services.dibbler.package.meta.mainProgram} loop
|
||||
|
||||
Password for dibbler is 'dibbler'
|
||||
|
||||
To exit, press Ctrl+A, then X
|
||||
=================================
|
||||
'';
|
||||
|
||||
users.users.dibbler = {
|
||||
isNormalUser = true;
|
||||
password = "dibbler";
|
||||
extraGroups = [ "wheel" ];
|
||||
};
|
||||
|
||||
services.getty.autologinUser = "dibbler";
|
||||
|
||||
programs.vim = {
|
||||
enable = true;
|
||||
defaultEditor = true;
|
||||
};
|
||||
|
||||
services.postgresql.enable = true;
|
||||
|
||||
services.dibbler = {
|
||||
enable = true;
|
||||
createLocalDatabase = true;
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{ self, nixpkgs, ... }:
|
||||
nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs {
|
||||
system = "x86_64-linux";
|
||||
overlays = [
|
||||
self.overlays.default
|
||||
];
|
||||
};
|
||||
modules = [
|
||||
"${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix"
|
||||
"${nixpkgs}/nixos/tests/common/user-account.nix"
|
||||
|
||||
self.nixosModules.default
|
||||
|
||||
({ config, ... }: {
|
||||
system.stateVersion = config.system.nixos.release;
|
||||
virtualisation.graphics = false;
|
||||
|
||||
services.postgresql.enable = true;
|
||||
|
||||
services.dibbler = {
|
||||
enable = true;
|
||||
createLocalDatabase = true;
|
||||
kioskMode = true;
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{ lib
|
||||
, python3Packages
|
||||
, makeWrapper
|
||||
, less
|
||||
, toybox
|
||||
}:
|
||||
let
|
||||
pyproject = builtins.fromTOML (builtins.readFile ../pyproject.toml);
|
||||
in
|
||||
python3Packages.buildPythonApplication {
|
||||
pname = pyproject.project.name;
|
||||
version = pyproject.project.version;
|
||||
src = lib.cleanSource ../.;
|
||||
|
||||
format = "pyproject";
|
||||
|
||||
# brother-ql is breaky breaky
|
||||
# https://github.com/NixOS/nixpkgs/issues/285234
|
||||
# dontCheckRuntimeDeps = true;
|
||||
|
||||
nativeBuildInputs = with python3Packages; [
|
||||
setuptools
|
||||
makeWrapper
|
||||
];
|
||||
propagatedBuildInputs = with python3Packages; [
|
||||
# brother-ql
|
||||
# matplotlib
|
||||
psycopg2-binary
|
||||
# python-barcode
|
||||
sqlalchemy
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
wrapProgram $out/bin/dibbler \
|
||||
--prefix PATH : "${lib.makeBinPath [
|
||||
less
|
||||
toybox # Needs `clear`
|
||||
]}"
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "The little kiosk that could";
|
||||
mainProgram = "dibbler";
|
||||
};
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
mkShell,
|
||||
python,
|
||||
ruff,
|
||||
uv,
|
||||
}:
|
||||
|
||||
mkShell {
|
||||
packages = [
|
||||
ruff
|
||||
uv
|
||||
(python.withPackages (ps: with ps; [
|
||||
# brother-ql
|
||||
# matplotlib
|
||||
psycopg2
|
||||
# python-barcode
|
||||
sqlalchemy
|
||||
]))
|
||||
];
|
||||
}
|
||||
@@ -1,26 +1,25 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
requires = ["setuptools", "setuptools-scm"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "dibbler"
|
||||
version = "1.0.0"
|
||||
authors = [
|
||||
{ name = "Programvareverkstedet", email = "projects@pvv.ntnu.no" }
|
||||
]
|
||||
authors = []
|
||||
description = "EDB-system for PVV"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = {text = "BSD-3-Clause"}
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
]
|
||||
dependencies = [
|
||||
"SQLAlchemy >= 2.0, <2.1",
|
||||
# "brother-ql",
|
||||
# "matplotlib",
|
||||
"psycopg2-binary >= 2.8, <2.10",
|
||||
# "python-barcode",
|
||||
"brother_ql_next",
|
||||
"matplotlib",
|
||||
"psycopg2 >= 2.8, <2.10",
|
||||
"python-barcode",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["dibbler*"]
|
||||
|
||||
168
uv.lock
generated
168
uv.lock
generated
@@ -1,168 +0,0 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "dibbler"
|
||||
version = "1.0.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "psycopg2-binary", specifier = ">=2.8,<2.10" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0,<2.1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.45"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user