Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91f924a75a | |||
| e1605aab29 | |||
| 1164d492a3 |
@@ -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
|
||||
|
||||
+5
-6
@@ -1,13 +1,12 @@
|
||||
result
|
||||
result-*
|
||||
**/__pycache__
|
||||
dibbler.egg-info
|
||||
.venv
|
||||
.direnv
|
||||
.devenv
|
||||
|
||||
dist
|
||||
|
||||
config.ini
|
||||
test.db
|
||||
|
||||
.ruff_cache
|
||||
|
||||
*.qcow2
|
||||
|
||||
dibbler/_version.py
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Robert Eric Maikher <robertem@pvv.ntnu.no> Robert Maikher <robertem342@gmail.com>
|
||||
Robert Eric Maikher <robertem@pvv.ntnu.no> robertem <3456661+RandomGamer342@users.noreply.github.com>
|
||||
|
||||
Øystein Kristoffer Tveit <oysteikt@pvv.ntnu.no> h7x4 <h7x4@nani.wtf>
|
||||
Øystein Kristoffer Tveit <oysteikt@pvv.ntnu.no> Oystein Kristoffer Tveit <oysteikt@pvv.ntnu.no>
|
||||
Øystein Kristoffer Tveit <oysteikt@pvv.ntnu.no> Øystein Tveit <h7x4@nani.wtf>
|
||||
@@ -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!
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{ pkgs ? import <nixos-unstable> { } }:
|
||||
{
|
||||
dibbler = pkgs.callPackage ./nix/dibbler.nix { };
|
||||
}
|
||||
+4
-54
@@ -1,56 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
# This module is supposed to act as a singleton and be filled
|
||||
# with config variables by cli.py
|
||||
|
||||
from dibbler.lib.helpers import file_is_submissive_and_readable
|
||||
import configparser
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path("/etc/dibbler/dibbler.toml")
|
||||
|
||||
|
||||
config: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
def load_config(config_path: Path | None = None) -> None:
|
||||
global config
|
||||
if config_path is not None:
|
||||
with Path(config_path).open("rb") as file:
|
||||
config = tomllib.load(file)
|
||||
elif file_is_submissive_and_readable(DEFAULT_CONFIG_PATH):
|
||||
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()}"
|
||||
|
||||
if 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}"
|
||||
return f"postgresql+psycopg2://{username}:{password}@{host}:{port}/{dbname}"
|
||||
print(f"Error: unknown database type '{db_type}'")
|
||||
exit(1)
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
@@ -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,108 +0,0 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import Engine, create_engine, inspect, select
|
||||
from sqlalchemy.exc import DBAPIError, OperationalError
|
||||
from sqlalchemy.orm import RelationshipProperty
|
||||
from sqlalchemy.orm.clsregistry import _ModuleMarker
|
||||
|
||||
from dibbler.lib.helpers import file_is_submissive_and_readable
|
||||
from dibbler.models import Base
|
||||
|
||||
|
||||
def check_db_health(engine: Engine, verify_table_existence: bool = False) -> None:
|
||||
dialect_name = getattr(engine.dialect, "name", "").lower()
|
||||
|
||||
if "postgres" in dialect_name:
|
||||
check_postgres_ping(engine)
|
||||
|
||||
elif dialect_name == "sqlite":
|
||||
check_sqlite_file(engine)
|
||||
|
||||
if verify_table_existence:
|
||||
verify_tables_and_columns(engine)
|
||||
|
||||
|
||||
def check_postgres_ping(engine: Engine) -> None:
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(select(1))
|
||||
scalar = result.scalar()
|
||||
if scalar != 1 and scalar is not None:
|
||||
print(
|
||||
"Unexpected response from Postgres when running 'SELECT 1'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
except (OperationalError, DBAPIError) as exc:
|
||||
print(f"Failed to connect to Postgres database: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_sqlite_file(engine: Engine) -> None:
|
||||
db_path = engine.url.database
|
||||
|
||||
# Don't verify in-memory databases or empty paths
|
||||
if db_path in (None, "", ":memory:"):
|
||||
return
|
||||
|
||||
db_path = db_path.removeprefix("file:").removeprefix("sqlite:")
|
||||
|
||||
# Strip query parameters
|
||||
if "?" in db_path:
|
||||
db_path = db_path.split("?", 1)[0]
|
||||
|
||||
path = Path(db_path)
|
||||
|
||||
if not path.exists():
|
||||
print(f"SQLite database file does not exist: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not path.is_file():
|
||||
print(f"SQLite database path is not a file: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not file_is_submissive_and_readable(path):
|
||||
print(f"SQLite database file is not submissive and readable: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def verify_tables_and_columns(engine: Engine) -> None:
|
||||
iengine = inspect(engine)
|
||||
errors = False
|
||||
tables = iengine.get_table_names()
|
||||
views = iengine.get_view_names()
|
||||
tables.extend(views)
|
||||
|
||||
for _name, klass in Base.registry._class_registry.items():
|
||||
if isinstance(klass, _ModuleMarker):
|
||||
continue
|
||||
|
||||
table = klass.__tablename__
|
||||
if table in tables:
|
||||
columns = [c["name"] for c in iengine.get_columns(table)]
|
||||
mapper = inspect(klass)
|
||||
|
||||
for column_prop in mapper.attrs:
|
||||
if isinstance(column_prop, RelationshipProperty):
|
||||
pass
|
||||
else:
|
||||
for column in column_prop.columns:
|
||||
if not column.key in columns:
|
||||
print(
|
||||
f"Model '{klass}' declares column '{column.key}' which does not exist in database {engine}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
errors = True
|
||||
else:
|
||||
print(
|
||||
f"Model '{klass}' declares table '{table}' which does not exist in database {engine}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
errors = True
|
||||
|
||||
if errors:
|
||||
print("Have you remembered to run `dibbler create-db?", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
+25
-60
@@ -1,69 +1,51 @@
|
||||
import os
|
||||
import pwd
|
||||
import signal
|
||||
import subprocess
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, 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,
|
||||
# NOTE: search_products has 3 parameters, but this one only have 2.
|
||||
# We need an extra parameter for polymorphic purposes.
|
||||
ignore_this_flag: None = None,
|
||||
) -> User | list[User] | 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
|
||||
return (
|
||||
sql_session.query(User)
|
||||
user_list = (
|
||||
session.query(User)
|
||||
.filter(
|
||||
or_(
|
||||
User.name.ilike(f"%{string}%"),
|
||||
User.card.ilike(f"%{string}%"),
|
||||
User.rfid.ilike(f"%{string}%"),
|
||||
),
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
return user_list
|
||||
|
||||
|
||||
def search_product(
|
||||
string: str,
|
||||
sql_session: Session,
|
||||
find_hidden_products: bool = True,
|
||||
) -> Product | list[Product] | None:
|
||||
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()
|
||||
)
|
||||
@@ -71,33 +53,30 @@ 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}%"),
|
||||
Product.name.ilike(f"%{string}%"),
|
||||
),
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
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()
|
||||
)
|
||||
return product_list
|
||||
|
||||
|
||||
def system_user_exists(username: str) -> bool:
|
||||
def system_user_exists(username):
|
||||
try:
|
||||
pwd.getpwnam(username)
|
||||
except KeyError:
|
||||
@@ -108,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:
|
||||
@@ -122,11 +101,7 @@ def guess_data_type(string: str) -> Literal["card", "rfid", "bar_code", "usernam
|
||||
return None
|
||||
|
||||
|
||||
def argmax(
|
||||
d: dict[Any, Any],
|
||||
all_: bool = False,
|
||||
value: Callable[[Any], Any] | None = None,
|
||||
) -> Any | list[Any] | None:
|
||||
def argmax(d, all=False, value=None):
|
||||
maxarg = None
|
||||
if value is not None:
|
||||
dd = d
|
||||
@@ -136,12 +111,12 @@ def argmax(
|
||||
for key in list(d.keys()):
|
||||
if maxarg is None or d[key] > d[maxarg]:
|
||||
maxarg = key
|
||||
if all_:
|
||||
if all:
|
||||
return [k for k in list(d.keys()) if d[k] == d[maxarg]]
|
||||
return maxarg
|
||||
|
||||
|
||||
def less(string: str) -> None:
|
||||
def less(string):
|
||||
"""
|
||||
Run less with string as input; wait until it finishes.
|
||||
"""
|
||||
@@ -153,13 +128,3 @@ def less(string: str) -> None:
|
||||
proc = subprocess.Popen("less", env=env, encoding="utf-8", stdin=subprocess.PIPE)
|
||||
proc.communicate(string)
|
||||
signal.signal(signal.SIGINT, int_handler)
|
||||
|
||||
|
||||
def file_is_submissive_and_readable(file: Path) -> bool:
|
||||
return file.is_file() and any(
|
||||
[
|
||||
file.stat().st_mode & 0o400 and file.stat().st_uid == os.getuid(),
|
||||
file.stat().st_mode & 0o040 and file.stat().st_gid == os.getgid(),
|
||||
file.stat().st_mode & 0o004,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,95 +1,96 @@
|
||||
# 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 os
|
||||
import datetime
|
||||
|
||||
# from .barcode_helpers import BrotherLabelWriter
|
||||
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
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -3,20 +3,18 @@
|
||||
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models import Transaction
|
||||
from .helpers import *
|
||||
from ..models import Transaction
|
||||
from ..db import Session
|
||||
|
||||
|
||||
def getUser(sql_session: Session) -> str:
|
||||
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
|
||||
@@ -39,11 +37,12 @@ def getUser(sql_session: Session) -> str:
|
||||
return user[n].name
|
||||
|
||||
|
||||
def getProduct(sql_session: Session) -> str:
|
||||
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
|
||||
@@ -77,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
|
||||
@@ -90,7 +93,7 @@ class Database:
|
||||
|
||||
|
||||
class InputLine:
|
||||
def __init__(self, u, p, t) -> None:
|
||||
def __init__(self, u, p, t):
|
||||
self.inputUser = u
|
||||
self.inputProduct = p
|
||||
self.inputType = t
|
||||
@@ -123,17 +126,17 @@ def getInputType():
|
||||
return int(inp)
|
||||
|
||||
|
||||
def getProducts(products: str) -> list[tuple[str]]:
|
||||
def getProducts(products):
|
||||
product = []
|
||||
split_products = products.partition("¤")
|
||||
products = products.partition("¤")
|
||||
product.append(products[0])
|
||||
while products[1] == "¤":
|
||||
split_products = split_products[2].partition("¤")
|
||||
products = products[2].partition("¤")
|
||||
product.append(products[0])
|
||||
return product
|
||||
|
||||
|
||||
def getDateFile(date: str, inp: str) -> datetime.date:
|
||||
def getDateFile(date, inp):
|
||||
try:
|
||||
year = inp.partition("-")
|
||||
month = year[2].partition("-")
|
||||
@@ -177,7 +180,7 @@ def addLineToDatabase(database, inputLine):
|
||||
if abs(inputLine.price) > 90000:
|
||||
return database
|
||||
# fyller inn for varer
|
||||
if (inputLine.product != "") and (
|
||||
if (not inputLine.product == "") and (
|
||||
(inputLine.inputProduct == "") or (inputLine.inputProduct == inputLine.product)
|
||||
):
|
||||
database.varePersonAntall[inputLine.product][inputLine.user] = (
|
||||
@@ -191,7 +194,7 @@ def addLineToDatabase(database, inputLine):
|
||||
database.vareUkedagAntall[inputLine.product][inputLine.weekday] += 1
|
||||
# fyller inn for personer
|
||||
if (inputLine.inputUser == "") or (inputLine.inputUser == inputLine.user):
|
||||
if inputLine.product != "":
|
||||
if not inputLine.product == "":
|
||||
database.personVareAntall[inputLine.user][inputLine.product] = (
|
||||
database.personVareAntall[inputLine.user].setdefault(inputLine.product, 0) + 1
|
||||
)
|
||||
@@ -215,7 +218,7 @@ def addLineToDatabase(database, inputLine):
|
||||
database.personNegTransactions[inputLine.user] = (
|
||||
database.personNegTransactions.setdefault(inputLine.user, 0) + inputLine.price
|
||||
)
|
||||
elif inputLine.inputType != 1:
|
||||
elif not (inputLine.inputType == 1):
|
||||
database.globalVareAntall[inputLine.product] = (
|
||||
database.globalVareAntall.setdefault(inputLine.product, 0) + 1
|
||||
)
|
||||
@@ -226,7 +229,7 @@ def addLineToDatabase(database, inputLine):
|
||||
# fyller inn for global statistikk
|
||||
if (inputLine.inputType == 3) or (inputLine.inputType == 4):
|
||||
database.pengebeholdning[inputLine.dateNum] += inputLine.price
|
||||
if inputLine.product != "":
|
||||
if not (inputLine.product == ""):
|
||||
database.globalPersonAntall[inputLine.user] = (
|
||||
database.globalPersonAntall.setdefault(inputLine.user, 0) + 1
|
||||
)
|
||||
@@ -239,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)
|
||||
@@ -274,9 +277,9 @@ def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session
|
||||
inputLine.price = 0
|
||||
|
||||
print("saving as default.dibblerlog...", end=" ")
|
||||
f = Path.open("default.dibblerlog", "w")
|
||||
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])
|
||||
@@ -291,7 +294,8 @@ def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session
|
||||
transaction.description,
|
||||
)
|
||||
f.write(line.encode("utf8"))
|
||||
f.close()
|
||||
session.close()
|
||||
f.close
|
||||
# bygg database.pengebeholdning
|
||||
if (inputType == 3) or (inputType == 4):
|
||||
for i in range(inputLine.numberOfDays + 1):
|
||||
@@ -311,7 +315,7 @@ def buildDatabaseFromFile(inputFile, inputType, inputProduct, inputUser):
|
||||
sdate = input("enter start date (yyyy-mm-dd)? ")
|
||||
edate = input("enter end date (yyyy-mm-dd)? ")
|
||||
|
||||
f = Path.open(inputFile)
|
||||
f = open(inputFile)
|
||||
try:
|
||||
fileLines = f.readlines()
|
||||
finally:
|
||||
@@ -329,7 +333,7 @@ def buildDatabaseFromFile(inputFile, inputType, inputProduct, inputUser):
|
||||
database.globalUkedagForbruk = [0] * 7
|
||||
database.pengebeholdning = [0] * (inputLine.numberOfDays + 1)
|
||||
for linje in fileLines:
|
||||
if linje[0] != "#" and linje != "\n":
|
||||
if not (linje[0] == "#") and not (linje == "\n"):
|
||||
# henter dateNum, products, user, price
|
||||
restDel = linje.partition("|")
|
||||
restDel = restDel[2].partition(" ")
|
||||
@@ -359,7 +363,7 @@ def buildDatabaseFromFile(inputFile, inputType, inputProduct, inputUser):
|
||||
return database, dateLine
|
||||
|
||||
|
||||
def printTopDict(dictionary: dict[str, Any], n: int, k: bool) -> None:
|
||||
def printTopDict(dictionary, n, k):
|
||||
i = 0
|
||||
for key in sorted(dictionary, key=dictionary.get, reverse=k):
|
||||
print(key, ": ", dictionary[key])
|
||||
@@ -369,7 +373,7 @@ def printTopDict(dictionary: dict[str, Any], n: int, k: bool) -> None:
|
||||
break
|
||||
|
||||
|
||||
def printTopDict2(dictionary, dictionary2, n) -> None:
|
||||
def printTopDict2(dictionary, dictionary2, n):
|
||||
print("")
|
||||
print("product : price[kr] ( number )")
|
||||
i = 0
|
||||
@@ -381,7 +385,7 @@ def printTopDict2(dictionary, dictionary2, n) -> None:
|
||||
break
|
||||
|
||||
|
||||
def printWeekdays(week, days) -> None:
|
||||
def printWeekdays(week, days):
|
||||
if week == [] or days == 0:
|
||||
return
|
||||
print(
|
||||
@@ -404,10 +408,10 @@ def printWeekdays(week, days) -> None:
|
||||
print("")
|
||||
|
||||
|
||||
def printBalance(database, user) -> None:
|
||||
def printBalance(database, user):
|
||||
forbruk = 0
|
||||
if user in database.personVareVerdi:
|
||||
forbruk = sum(database.personVareVerdi[user].values())
|
||||
forbruk = sum([i for i in list(database.personVareVerdi[user].values())])
|
||||
print("totalt kjøpt for: ", forbruk, end=" ")
|
||||
if user in database.personNegTransactions:
|
||||
print("kr, totalt lagt til: ", -database.personNegTransactions[user], end=" ")
|
||||
@@ -419,14 +423,14 @@ def printBalance(database, user) -> None:
|
||||
print("")
|
||||
|
||||
|
||||
def printUser(database, dateLine, user, n) -> None:
|
||||
def printUser(database, dateLine, user, n):
|
||||
printTopDict2(database.personVareVerdi[user], database.personVareAntall[user], n)
|
||||
print("\nforbruk per ukedag [kr/dag],", end=" ")
|
||||
printWeekdays(database.personUkedagVerdi[user], len(dateLine))
|
||||
printBalance(database, user)
|
||||
|
||||
|
||||
def printProduct(database, dateLine, product, n) -> None:
|
||||
def printProduct(database, dateLine, product, n):
|
||||
printTopDict(database.varePersonAntall[product], n, 1)
|
||||
print("\nforbruk per ukedag [antall/dag],", end=" ")
|
||||
printWeekdays(database.vareUkedagAntall[product], len(dateLine))
|
||||
@@ -440,7 +444,7 @@ def printProduct(database, dateLine, product, n) -> None:
|
||||
)
|
||||
|
||||
|
||||
def printGlobal(database, dateLine, n) -> None:
|
||||
def printGlobal(database, dateLine, n):
|
||||
print("\nmest lagt til: ")
|
||||
printTopDict(database.personNegTransactions, n, 0)
|
||||
print("\nmest tatt fra:")
|
||||
@@ -454,9 +458,9 @@ def printGlobal(database, dateLine, n) -> None:
|
||||
"Det er solgt varer til en verdi av: ",
|
||||
sum(database.globalDatoForbruk),
|
||||
"kr, det er lagt til",
|
||||
-sum(database.personNegTransactions.values()),
|
||||
-sum([i for i in list(database.personNegTransactions.values())]),
|
||||
"og tatt fra",
|
||||
sum(database.personPosTransactions.values()),
|
||||
sum([i for i in list(database.personPosTransactions.values())]),
|
||||
end=" ",
|
||||
)
|
||||
print(
|
||||
@@ -466,24 +470,23 @@ def printGlobal(database, dateLine, n) -> None:
|
||||
)
|
||||
|
||||
|
||||
def alt4menuTextOnly(database, dateLine, sql_session: Session) -> None:
|
||||
assert sql_session is not None
|
||||
def alt4menuTextOnly(database, dateLine):
|
||||
n = 10
|
||||
while 1:
|
||||
print(
|
||||
"\n1: user-statistics, 2: product-statistics, 3:global-statistics, n: adjust amount of data shown q:quit",
|
||||
"\n1: user-statistics, 2: product-statistics, 3:global-statistics, n: adjust amount of data shown q:quit"
|
||||
)
|
||||
inp = input("")
|
||||
if inp == "q":
|
||||
break
|
||||
if inp == "1":
|
||||
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":
|
||||
@@ -495,16 +498,15 @@ def alt4menuTextOnly(database, dateLine, sql_session: Session) -> None:
|
||||
n = int(input("set number to show "))
|
||||
|
||||
|
||||
def statisticsTextOnly(sql_session: Session) -> None:
|
||||
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 inp != "q":
|
||||
alt4menuTextOnly(database, dateLine, sql_session)
|
||||
if not inp == "q":
|
||||
alt4menuTextOnly(database, dateLine)
|
||||
|
||||
+12
-53
@@ -1,12 +1,8 @@
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.conf import config_db_string, load_config
|
||||
from dibbler.lib.check_db_health import check_db_health
|
||||
from dibbler.conf import config
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
@@ -14,78 +10,41 @@ parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
help="Path to the config file",
|
||||
type=Path,
|
||||
metavar="FILE",
|
||||
type=str,
|
||||
required=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-V",
|
||||
"--version",
|
||||
help="Show program version",
|
||||
action="store_true",
|
||||
default=False,
|
||||
default=os.environ.get("DIBBLER_CONFIG_FILE", None)
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
title="subcommands",
|
||||
dest="subcommand",
|
||||
required=True,
|
||||
)
|
||||
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() -> None:
|
||||
def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version:
|
||||
from ._version import commit_id, version
|
||||
|
||||
print(f"Dibbler version {version}, commit {commit_id if commit_id else '<unknown>'}")
|
||||
return
|
||||
|
||||
if not args.subcommand:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
load_config(args.config)
|
||||
|
||||
engine = create_engine(config_db_string())
|
||||
|
||||
sql_session = Session(
|
||||
engine,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
close_resets_only=True,
|
||||
)
|
||||
|
||||
check_db_health(
|
||||
engine,
|
||||
verify_table_existence=args.subcommand != "create-db",
|
||||
)
|
||||
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__":
|
||||
|
||||
@@ -26,28 +26,28 @@ __all__ = [
|
||||
from .addstock import AddStockMenu
|
||||
from .buymenu import BuyMenu
|
||||
from .editing import (
|
||||
AddProductMenu,
|
||||
AddUserMenu,
|
||||
EditUserMenu,
|
||||
AddProductMenu,
|
||||
EditProductMenu,
|
||||
AdjustStockMenu,
|
||||
CleanupStockMenu,
|
||||
EditProductMenu,
|
||||
EditUserMenu,
|
||||
)
|
||||
from .faq import FAQMenu
|
||||
from .helpermenus import Menu
|
||||
from .mainmenu import MainMenu
|
||||
from .miscmenus import (
|
||||
AdjustCreditMenu,
|
||||
ProductListMenu,
|
||||
ProductSearchMenu,
|
||||
ShowUserMenu,
|
||||
TransferMenu,
|
||||
AdjustCreditMenu,
|
||||
UserListMenu,
|
||||
ShowUserMenu,
|
||||
ProductListMenu,
|
||||
)
|
||||
from .printermenu import PrintLabelMenu
|
||||
from .stats import (
|
||||
BalanceMenu,
|
||||
LoggedStatisticsMenu,
|
||||
ProductPopularityMenu,
|
||||
ProductRevenueMenu,
|
||||
BalanceMenu,
|
||||
LoggedStatisticsMenu,
|
||||
)
|
||||
|
||||
+12
-21
@@ -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) -> None:
|
||||
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"""
|
||||
@@ -25,7 +23,7 @@ much money you're due in credits for the purchase when prompted.\n"""
|
||||
self.products = {}
|
||||
self.price = 0
|
||||
|
||||
def _execute(self, **_kwargs) -> bool | None:
|
||||
def _execute(self):
|
||||
questions = {
|
||||
(
|
||||
False,
|
||||
@@ -88,10 +86,10 @@ much money you're due in credits for the purchase when prompted.\n"""
|
||||
|
||||
self.perform_transaction()
|
||||
|
||||
def complete_input(self) -> bool:
|
||||
return self.users is not None and len(self.products) > 0 and self.price > 0
|
||||
def complete_input(self):
|
||||
return bool(self.users) and len(self.products) and self.price
|
||||
|
||||
def print_info(self) -> None:
|
||||
def print_info(self):
|
||||
width = 6 + Product.name_length
|
||||
print()
|
||||
print(width * "-")
|
||||
@@ -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,
|
||||
) -> None:
|
||||
def add_thing_to_pending(self, thing, amount, price):
|
||||
if isinstance(thing, User):
|
||||
self.users.append(thing)
|
||||
elif thing in list(self.products.keys()):
|
||||
@@ -126,7 +119,7 @@ much money you're due in credits for the purchase when prompted.\n"""
|
||||
else:
|
||||
self.products[thing] = [amount, price]
|
||||
|
||||
def perform_transaction(self) -> None:
|
||||
def perform_transaction(self):
|
||||
print("Did you pay a different price?")
|
||||
if self.confirm(">", default=False):
|
||||
self.price = self.input_int("How much did you pay?", 0, self.price, default=self.price)
|
||||
@@ -139,11 +132,10 @@ much money you're due in credits for the purchase when prompted.\n"""
|
||||
old_price = product.price
|
||||
old_hidden = product.hidden
|
||||
product.price = int(
|
||||
ceil(float(value) / (max(product.stock, 0) + self.products[product][0])),
|
||||
ceil(float(value) / (max(product.stock, 0) + self.products[product][0]))
|
||||
)
|
||||
product.stock = max(
|
||||
self.products[product][0],
|
||||
product.stock + self.products[product][0],
|
||||
self.products[product][0], product.stock + self.products[product][0]
|
||||
)
|
||||
product.hidden = False
|
||||
print(
|
||||
@@ -159,14 +151,13 @@ 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:
|
||||
print(f"User {user.name}'s credit is now {user.credit:d}")
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
self.sql_session.rollback()
|
||||
print(f"Could not perform transaction: {e}")
|
||||
|
||||
+46
-69
@@ -1,8 +1,4 @@
|
||||
from typing import Any
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.conf import config
|
||||
from dibbler.models import (
|
||||
@@ -17,11 +13,10 @@ from .helpermenus import Menu
|
||||
|
||||
|
||||
class BuyMenu(Menu):
|
||||
superfast_mode: bool
|
||||
purchase: Purchase
|
||||
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
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.
|
||||
@@ -33,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) -> bool:
|
||||
def credit_check(user):
|
||||
"""
|
||||
|
||||
:param user:
|
||||
@@ -42,32 +37,28 @@ 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,
|
||||
) -> bool:
|
||||
def low_credit_warning(self, user, timeout=False):
|
||||
assert isinstance(user, User)
|
||||
|
||||
print(r"***********************************************************************")
|
||||
print(r"***********************************************************************")
|
||||
print(r"")
|
||||
print(r"$$\ $$\ $$$$$$\ $$$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$\\")
|
||||
print(r"$$ | $\ $$ |$$ __$$\ $$ __$$\ $$$\ $$ |\_$$ _|$$$\ $$ |$$ __$$\\")
|
||||
print(r"$$ |$$$\ $$ |$$ / $$ |$$ | $$ |$$$$\ $$ | $$ | $$$$\ $$ |$$ / \__|")
|
||||
print(r"$$ $$ $$\$$ |$$$$$$$$ |$$$$$$$ |$$ $$\$$ | $$ | $$ $$\$$ |$$ |$$$$\\")
|
||||
print(r"$$$$ _$$$$ |$$ __$$ |$$ __$$< $$ \$$$$ | $$ | $$ \$$$$ |$$ |\_$$ |")
|
||||
print(r"$$$ / \$$$ |$$ | $$ |$$ | $$ |$$ |\$$$ | $$ | $$ |\$$$ |$$ | $$ |")
|
||||
print(r"$$ / \$$ |$$ | $$ |$$ | $$ |$$ | \$$ |$$$$$$\ $$ | \$$ |\$$$$$$ |")
|
||||
print(r"\__/ \__|\__| \__|\__| \__|\__| \__|\______|\__| \__| \______/")
|
||||
print(r"")
|
||||
print(r"***********************************************************************")
|
||||
print(r"***********************************************************************")
|
||||
print(r"")
|
||||
print("***********************************************************************")
|
||||
print("***********************************************************************")
|
||||
print("")
|
||||
print("$$\ $$\ $$$$$$\ $$$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$\\")
|
||||
print("$$ | $\ $$ |$$ __$$\ $$ __$$\ $$$\ $$ |\_$$ _|$$$\ $$ |$$ __$$\\")
|
||||
print("$$ |$$$\ $$ |$$ / $$ |$$ | $$ |$$$$\ $$ | $$ | $$$$\ $$ |$$ / \__|")
|
||||
print("$$ $$ $$\$$ |$$$$$$$$ |$$$$$$$ |$$ $$\$$ | $$ | $$ $$\$$ |$$ |$$$$\\")
|
||||
print("$$$$ _$$$$ |$$ __$$ |$$ __$$< $$ \$$$$ | $$ | $$ \$$$$ |$$ |\_$$ |")
|
||||
print("$$$ / \$$$ |$$ | $$ |$$ | $$ |$$ |\$$$ | $$ | $$ |\$$$ |$$ | $$ |")
|
||||
print("$$ / \$$ |$$ | $$ |$$ | $$ |$$ | \$$ |$$$$$$\ $$ | \$$ |\$$$$$$ |")
|
||||
print("\__/ \__|\__| \__|\__| \__|\__| \__|\______|\__| \__| \______/")
|
||||
print("")
|
||||
print("***********************************************************************")
|
||||
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,13 +68,10 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
if timeout:
|
||||
print("THIS PURCHASE WILL AUTOMATICALLY BE PERFORMED IN 3 MINUTES!")
|
||||
return self.confirm(prompt=">", default=True, timeout=180)
|
||||
return self.confirm(prompt=">", default=True)
|
||||
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("---------------------------------------------")
|
||||
@@ -92,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
|
||||
@@ -110,11 +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,
|
||||
**_kwargs,
|
||||
) -> bool:
|
||||
def _execute(self, initial_contents=None):
|
||||
self.print_header()
|
||||
self.purchase = Purchase()
|
||||
self.exit_confirm_msg = None
|
||||
@@ -126,7 +107,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
for thing, num in initial_contents:
|
||||
self.add_thing_to_purchase(thing, num)
|
||||
|
||||
def is_product(candidate: Any) -> bool:
|
||||
def is_product(candidate):
|
||||
return isinstance(candidate[0], Product)
|
||||
|
||||
if len(initial_contents) > 0 and all(map(is_product, initial_contents)):
|
||||
@@ -148,7 +129,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
True,
|
||||
True,
|
||||
): "Enter more products or users, or an empty line to confirm",
|
||||
}[(len(self.purchase.transactions) > 0, len(self.purchase.entries) > 0)],
|
||||
}[(len(self.purchase.transactions) > 0, len(self.purchase.entries) > 0)]
|
||||
)
|
||||
|
||||
# Read in a 'thing' (product or user):
|
||||
@@ -166,16 +147,16 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
if thing is None:
|
||||
if not self.complete_input():
|
||||
if self.confirm(
|
||||
"Not enough information entered. Abort purchase?",
|
||||
default=True,
|
||||
"Not enough information entered. Abort purchase?", default=True
|
||||
):
|
||||
return False
|
||||
continue
|
||||
break
|
||||
# once we get something in the
|
||||
# purchase, we want to protect the
|
||||
# user from accidentally killing it
|
||||
self.exit_confirm_msg = "Abort purchase?"
|
||||
else:
|
||||
# once we get something in the
|
||||
# purchase, we want to protect the
|
||||
# user from accidentally killing it
|
||||
self.exit_confirm_msg = "Abort purchase?"
|
||||
|
||||
# Add the thing to our purchase object:
|
||||
if not self.add_thing_to_purchase(thing, amount=num):
|
||||
@@ -186,11 +167,10 @@ 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()
|
||||
except SQLAlchemyError as e:
|
||||
self.sql_session.rollback()
|
||||
self.session.commit()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store purchase: {e}")
|
||||
else:
|
||||
print("Purchase stored.")
|
||||
@@ -198,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.",
|
||||
)
|
||||
|
||||
@@ -209,10 +189,10 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
print("")
|
||||
return True
|
||||
|
||||
def complete_input(self) -> bool:
|
||||
def complete_input(self):
|
||||
return self.purchase.is_complete()
|
||||
|
||||
def format_purchase(self) -> str | None:
|
||||
def format_purchase(self):
|
||||
self.purchase.set_price()
|
||||
transactions = self.purchase.transactions
|
||||
entries = self.purchase.entries
|
||||
@@ -224,10 +204,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
string += "(empty)"
|
||||
else:
|
||||
string += ", ".join(
|
||||
[
|
||||
t.user.name + ("*" if not self.credit_check(t.user) else "")
|
||||
for t in transactions
|
||||
],
|
||||
[t.user.name + ("*" if not self.credit_check(t.user) else "") for t in transactions]
|
||||
)
|
||||
string += "\n products: "
|
||||
if len(entries) == 0:
|
||||
@@ -235,7 +212,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
else:
|
||||
string += "\n "
|
||||
string += "\n ".join(
|
||||
[f"{e.amount:d}x {e.product.name} ({e.product.price:d} kr)" for e in entries],
|
||||
[f"{e.amount:d}x {e.product.name} ({e.product.price:d} kr)" for e in entries]
|
||||
)
|
||||
if len(transactions) > 1:
|
||||
string += f"\n price per person: {self.purchase.price_per_transaction():d} kr"
|
||||
@@ -251,7 +228,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
||||
|
||||
return string
|
||||
|
||||
def print_purchase(self) -> None:
|
||||
def print_purchase(self):
|
||||
info = self.format_purchase()
|
||||
if info is not None:
|
||||
self.set_context(info)
|
||||
|
||||
+39
-68
@@ -1,9 +1,6 @@
|
||||
import sqlalchemy
|
||||
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.models import Product, User
|
||||
|
||||
from dibbler.models import User, Product
|
||||
from .helpermenus import Menu, Selector
|
||||
|
||||
__all__ = [
|
||||
@@ -17,48 +14,32 @@ __all__ = [
|
||||
|
||||
|
||||
class AddUserMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Add user", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Add user", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
username = self.input_str(
|
||||
"Username (should be same as PVV username)",
|
||||
regex=User.name_re,
|
||||
length_range=(1, 10),
|
||||
)
|
||||
assert username is not None
|
||||
|
||||
cardnum = self.input_str(
|
||||
"Card number (optional)",
|
||||
regex=User.card_re,
|
||||
length_range=(0, 10),
|
||||
empty_string_is_none=True,
|
||||
)
|
||||
if cardnum is not None:
|
||||
cardnum = cardnum.lower()
|
||||
|
||||
rfid = self.input_str(
|
||||
"RFID (optional)",
|
||||
regex=User.rfid_re,
|
||||
length_range=(0, 10),
|
||||
empty_string_is_none=True,
|
||||
)
|
||||
|
||||
cardnum = self.input_str("Card number (optional)", regex=User.card_re, length_range=(0, 10))
|
||||
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 IntegrityError as e:
|
||||
self.sql_session.rollback()
|
||||
except sqlalchemy.exc.IntegrityError as e:
|
||||
print(f"Could not store user {username}: {e}")
|
||||
self.pause()
|
||||
|
||||
|
||||
class EditUserMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
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.
|
||||
|
||||
@@ -66,7 +47,7 @@ First select an existing user, then enter a new card number for that
|
||||
user, then rfid (write an empty line to remove the card number or rfid).
|
||||
"""
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
user = self.input_user("User")
|
||||
self.printc(f"Editing user {user.name}")
|
||||
@@ -88,50 +69,43 @@ 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 SQLAlchemyError as e:
|
||||
self.sql_session.rollback()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store user {user.name}: {e}")
|
||||
self.pause()
|
||||
|
||||
|
||||
class AddProductMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Add product", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Add product", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
bar_code = self.input_str("Bar code", regex=Product.bar_code_re, length_range=(8, 13))
|
||||
assert bar_code is not None
|
||||
|
||||
name = self.input_str("Name", regex=Product.name_re, length_range=(1, Product.name_length))
|
||||
assert name is not None
|
||||
|
||||
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 SQLAlchemyError as e:
|
||||
self.sql_session.rollback()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store product {name}: {e}")
|
||||
self.pause()
|
||||
|
||||
|
||||
class EditProductMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Edit product", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Edit product", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
product = self.input_product("Product")
|
||||
self.printc(f"Editing product {product.name}")
|
||||
while True:
|
||||
selector = Selector(
|
||||
f"Do what with {product.name}?",
|
||||
sql_session=self.sql_session,
|
||||
items=[
|
||||
("name", "Edit name"),
|
||||
("price", "Edit price"),
|
||||
@@ -161,10 +135,9 @@ 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 SQLAlchemyError as e:
|
||||
self.sql_session.rollback()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store product {product.name}: {e}")
|
||||
self.pause()
|
||||
return
|
||||
@@ -176,10 +149,10 @@ class EditProductMenu(Menu):
|
||||
|
||||
|
||||
class AdjustStockMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Adjust stock", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Adjust stock", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
product = self.input_product("Product")
|
||||
|
||||
@@ -190,16 +163,15 @@ class AdjustStockMenu(Menu):
|
||||
if add_stock > 0:
|
||||
print(f"You added {add_stock:d} to the stock of {product}")
|
||||
else:
|
||||
print(f"You removed {(add_stock * -1):d} from the stock of {product}")
|
||||
print(f"You removed {add_stock:d} from the stock of {product}")
|
||||
|
||||
product.stock += add_stock
|
||||
|
||||
try:
|
||||
self.sql_session.commit()
|
||||
self.session.commit()
|
||||
print("Stock is now stored")
|
||||
self.pause()
|
||||
except SQLAlchemyError as e:
|
||||
self.sql_session.rollback()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store stock: {e}")
|
||||
self.pause()
|
||||
return
|
||||
@@ -207,13 +179,13 @@ class AdjustStockMenu(Menu):
|
||||
|
||||
|
||||
class CleanupStockMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Stock Cleanup", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Stock Cleanup", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
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.")
|
||||
@@ -227,16 +199,15 @@ 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 SQLAlchemyError as e:
|
||||
self.sql_session.rollback()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store stock: {e}")
|
||||
self.pause()
|
||||
return
|
||||
|
||||
+70
-87
@@ -1,146 +1,129 @@
|
||||
from textwrap import dedent
|
||||
# -*- 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) -> None:
|
||||
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?",
|
||||
dedent("""
|
||||
We want to avoid keeping lots of cash in PVVVV's money box and to
|
||||
make it easy to pay for stuff without using money. (Without using
|
||||
money each time, that is. You do of course have to pay for the things
|
||||
you buy eventually).
|
||||
"""
|
||||
We want to avoid keeping lots of cash in PVVVV's money box and to
|
||||
make it easy to pay for stuff without using money. (Without using
|
||||
money each time, that is. You do of course have to pay for the things
|
||||
you buy eventually).
|
||||
|
||||
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".
|
||||
Alternatively, add money to the money box and use "Adjust credit" to
|
||||
tell Dibbler about it.
|
||||
"""),
|
||||
sql_session,
|
||||
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".
|
||||
Alternatively, add money to the money box and use "Adjust credit" to
|
||||
tell Dibbler about it.
|
||||
""",
|
||||
),
|
||||
MessageMenu(
|
||||
"Can I still pay for stuff using cash?",
|
||||
dedent("""
|
||||
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,
|
||||
"""
|
||||
Please put money in the money box and use "Adjust Credit" so that
|
||||
dibbler can keep track of credit and purchases.""",
|
||||
),
|
||||
MessageMenu("How do I exit from a submenu/dialog/thing?", 'Type "exit", "q", or ^d.'),
|
||||
MessageMenu(
|
||||
'What does "." mean?',
|
||||
dedent("""
|
||||
The "." character, known as "full stop" or "period", is most often
|
||||
used to indicate the end of a sentence.
|
||||
"""
|
||||
The "." character, known as "full stop" or "period", is most often
|
||||
used to indicate the end of a sentence.
|
||||
|
||||
It is also used by Dibbler to indicate that the program wants you to
|
||||
read some text before continuing. Whenever some output ends with a
|
||||
line containing only a period, you should read the lines above and
|
||||
then press enter to continue.
|
||||
"""),
|
||||
sql_session,
|
||||
It is also used by Dibbler to indicate that the program wants you to
|
||||
read some text before continuing. Whenever some output ends with a
|
||||
line containing only a period, you should read the lines above and
|
||||
then press enter to continue.
|
||||
""",
|
||||
),
|
||||
MessageMenu(
|
||||
"Why is the user interface so terribly unintuitive?",
|
||||
dedent("""
|
||||
Answer #1: It is not.
|
||||
"""
|
||||
Answer #1: It is not.
|
||||
|
||||
Answer #2: We are trying to compete with PVV's microwave oven in
|
||||
userfriendliness.
|
||||
Answer #2: We are trying to compete with PVV's microwave oven in
|
||||
userfriendliness.
|
||||
|
||||
Answer #3: YOU are unintuitive.
|
||||
"""),
|
||||
sql_session,
|
||||
Answer #3: YOU are unintuitive.
|
||||
""",
|
||||
),
|
||||
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?",
|
||||
dedent("""
|
||||
Type "c" on the main menu to change the colours of the display, or
|
||||
"cs" if you are a boring person.
|
||||
"""),
|
||||
sql_session,
|
||||
"""
|
||||
Type "c" on the main menu to change the colours of the display, or
|
||||
"cs" if you are a boring person.
|
||||
""",
|
||||
),
|
||||
MessageMenu(
|
||||
"I found a bug; is there a reward?",
|
||||
dedent("""
|
||||
No.
|
||||
"""
|
||||
No.
|
||||
|
||||
But if you are certain that it is a bug, not a feature, then you
|
||||
should fix it (or better: force someone else to do it).
|
||||
But if you are certain that it is a bug, not a feature, then you
|
||||
should fix it (or better: force someone else to do it).
|
||||
|
||||
Follow this procedure:
|
||||
Follow this procedure:
|
||||
|
||||
1. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
|
||||
1. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
|
||||
|
||||
2. Fix the bug.
|
||||
2. Fix the bug.
|
||||
|
||||
3. Check that the program still runs (and, preferably, that the bug is
|
||||
in fact fixed).
|
||||
3. Check that the program still runs (and, preferably, that the bug is
|
||||
in fact fixed).
|
||||
|
||||
4. Commit.
|
||||
4. Commit.
|
||||
|
||||
5. Update the running copy from svn:
|
||||
5. Update the running copy from svn:
|
||||
|
||||
$ su -
|
||||
# su -l -s /bin/bash pvvvv
|
||||
$ cd dibbler
|
||||
$ git pull
|
||||
$ su -
|
||||
# su -l -s /bin/bash pvvvv
|
||||
$ cd dibbler
|
||||
$ git pull
|
||||
|
||||
6. Type "restart" in Dibbler to replace the running process by a new
|
||||
one using the updated files.
|
||||
"""),
|
||||
sql_session,
|
||||
6. Type "restart" in Dibbler to replace the running process by a new
|
||||
one using the updated files.
|
||||
""",
|
||||
),
|
||||
MessageMenu(
|
||||
"My question isn't listed here; what do I do?",
|
||||
dedent("""
|
||||
DON'T PANIC.
|
||||
"""
|
||||
DON'T PANIC.
|
||||
|
||||
Follow this procedure:
|
||||
Follow this procedure:
|
||||
|
||||
1. Ask someone (or read the source code) and get an answer.
|
||||
1. Ask someone (or read the source code) and get an answer.
|
||||
|
||||
2. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
|
||||
2. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
|
||||
|
||||
3. Add your question (with answer) to the FAQ and commit.
|
||||
3. Add your question (with answer) to the FAQ and commit.
|
||||
|
||||
4. Update the running copy from svn:
|
||||
4. Update the running copy from svn:
|
||||
|
||||
$ su -
|
||||
# su -l -s /bin/bash pvvvv
|
||||
$ cd dibbler
|
||||
$ git pull
|
||||
$ su -
|
||||
# su -l -s /bin/bash pvvvv
|
||||
$ cd dibbler
|
||||
$ git pull
|
||||
|
||||
5. Type "restart" in Dibbler to replace the running process by a new
|
||||
one using the updated files.
|
||||
"""),
|
||||
sql_session,
|
||||
5. Type "restart" in Dibbler to replace the running process by a new
|
||||
one using the updated files.
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
||||
+166
-280
@@ -1,64 +1,44 @@
|
||||
from __future__ import annotations
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import re
|
||||
import sys
|
||||
from select import select
|
||||
from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar
|
||||
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
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
|
||||
|
||||
|
||||
MenuItemType = TypeVar("MenuItemType", bound="Menu")
|
||||
|
||||
|
||||
class Menu:
|
||||
name: str
|
||||
sql_session: Session
|
||||
items: list[Menu | tuple[MenuItemType, str] | 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
|
||||
|
||||
class Menu(object):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
sql_session: Session,
|
||||
items: list[Self | tuple[MenuItemType, 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,
|
||||
) -> None:
|
||||
self.name: str = name
|
||||
self.sql_session: Session = sql_session
|
||||
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 = name
|
||||
self.items = items if items is not None else []
|
||||
self.prompt = prompt
|
||||
self.end_prompt = end_prompt
|
||||
@@ -68,61 +48,54 @@ class Menu:
|
||||
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
|
||||
if isinstance(self.items[i], tuple):
|
||||
elif isinstance(self.items[i], tuple):
|
||||
return self.items[i][1]
|
||||
return self.items[i]
|
||||
else:
|
||||
return self.items[i]
|
||||
|
||||
def item_value(self, i: int) -> MenuItemType | int:
|
||||
def item_value(self, i):
|
||||
if isinstance(self.items[i], tuple):
|
||||
return self.items[i][0]
|
||||
if self.return_index:
|
||||
@@ -131,14 +104,14 @@ class Menu:
|
||||
|
||||
def input_str(
|
||||
self,
|
||||
prompt: str | None = None,
|
||||
end_prompt: str | None = None,
|
||||
regex: str | None = None,
|
||||
length_range: tuple[int | None, int | None] = (None, None),
|
||||
empty_string_is_none: bool = False,
|
||||
timeout: int | None = None,
|
||||
default: str | None = None,
|
||||
) -> str | None:
|
||||
prompt=None,
|
||||
end_prompt=None,
|
||||
regex=None,
|
||||
length_range=(None, None),
|
||||
empty_string_is_none=False,
|
||||
timeout=None,
|
||||
default=None,
|
||||
):
|
||||
if prompt is None:
|
||||
prompt = self.prompt if self.prompt is not None else ""
|
||||
if default is not None:
|
||||
@@ -195,7 +168,7 @@ class Menu:
|
||||
):
|
||||
if length_range[0] and length_range[1]:
|
||||
print(
|
||||
f"Value must have length in range [{length_range[0]:d}, {length_range[1]:d}]",
|
||||
f"Value must have length in range [{length_range[0]:d}, {length_range[1]:d}]"
|
||||
)
|
||||
elif length_range[0]:
|
||||
print(f"Value must have length at least {length_range[0]:d}")
|
||||
@@ -204,7 +177,7 @@ class Menu:
|
||||
continue
|
||||
return result
|
||||
|
||||
def special_input_options(self, result) -> bool:
|
||||
def special_input_options(self, result):
|
||||
"""
|
||||
Handles special, magic input for input_str
|
||||
|
||||
@@ -214,7 +187,7 @@ class Menu:
|
||||
"""
|
||||
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.
|
||||
|
||||
@@ -224,39 +197,33 @@ class Menu:
|
||||
"""
|
||||
return False
|
||||
|
||||
def input_choice(
|
||||
self,
|
||||
number_of_choices: int,
|
||||
prompt: str | None = None,
|
||||
end_prompt: str | None = None,
|
||||
) -> int:
|
||||
def input_choice(self, number_of_choices, prompt=None, end_prompt=None):
|
||||
while True:
|
||||
result = self.input_str(prompt, end_prompt)
|
||||
assert result is not None
|
||||
if result == "":
|
||||
print("Please enter something")
|
||||
else:
|
||||
if result.isdigit():
|
||||
choice = int(result)
|
||||
if choice == 0 and number_of_choices >= 10:
|
||||
if choice == 0 and 10 <= number_of_choices:
|
||||
return 10
|
||||
if 0 < choice <= number_of_choices:
|
||||
return choice
|
||||
if not self.special_input_choice(result):
|
||||
self.invalid_menu_choice(result)
|
||||
|
||||
def invalid_menu_choice(self, in_str: str) -> None:
|
||||
def invalid_menu_choice(self, in_str):
|
||||
print("Please enter a valid choice.")
|
||||
|
||||
def input_int(
|
||||
self,
|
||||
prompt: str,
|
||||
minimum: int | None = None,
|
||||
maximum: int | None = None,
|
||||
null_allowed: bool = False,
|
||||
zero_allowed: bool = True,
|
||||
default: int | None = None,
|
||||
) -> int | Literal[False]:
|
||||
prompt=None,
|
||||
minimum=None,
|
||||
maximum=None,
|
||||
null_allowed=False,
|
||||
zero_allowed=True,
|
||||
default=None,
|
||||
):
|
||||
if minimum is not None and maximum is not None:
|
||||
end_prompt = f"({minimum}-{maximum})>"
|
||||
elif minimum is not None:
|
||||
@@ -267,11 +234,7 @@ class Menu:
|
||||
end_prompt = ""
|
||||
|
||||
while True:
|
||||
result = self.input_str(
|
||||
prompt + end_prompt,
|
||||
default=str(default) if default is not None else None,
|
||||
)
|
||||
assert result is not None
|
||||
result = self.input_str(prompt + end_prompt, default=default)
|
||||
if result == "" and null_allowed:
|
||||
return False
|
||||
try:
|
||||
@@ -289,117 +252,93 @@ class Menu:
|
||||
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:
|
||||
search_string = self.input_str(prompt, end_prompt, empty_string_is_none = True)
|
||||
if search_string is None:
|
||||
print("Please write something")
|
||||
continue
|
||||
user = self.retrieve_user(search_string)
|
||||
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:
|
||||
search_string = self.input_str(prompt, end_prompt)
|
||||
assert search_string is not None
|
||||
product = self.retrieve_product(search_string)
|
||||
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"),
|
||||
add_nonexisting: Iterable[str] = (),
|
||||
empty_input_permitted: bool = False,
|
||||
find_hidden_products: bool = True,
|
||||
) -> User | Product | None:
|
||||
prompt=None,
|
||||
end_prompt=None,
|
||||
permitted_things=("user", "product"),
|
||||
add_nonexisting=(),
|
||||
empty_input_permitted=False,
|
||||
find_hidden_products=True,
|
||||
):
|
||||
result = None
|
||||
while result is None:
|
||||
search_str = self.input_str(prompt, end_prompt)
|
||||
assert search_str is not None
|
||||
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"),
|
||||
add_nonexisting: Iterable[str] = (),
|
||||
empty_input_permitted: bool = False,
|
||||
find_hidden_products: bool = True,
|
||||
) -> tuple[User | Product, int] | None:
|
||||
prompt=None,
|
||||
end_prompt=None,
|
||||
permitted_things=("user", "product"),
|
||||
add_nonexisting=(),
|
||||
empty_input_permitted=False,
|
||||
find_hidden_products=True,
|
||||
):
|
||||
result = None
|
||||
num = 0
|
||||
while result is None:
|
||||
search_str = self.input_str(prompt, end_prompt)
|
||||
assert search_str is not None
|
||||
search_lst = search_str.split(" ")
|
||||
if search_str == "" and empty_input_permitted:
|
||||
return None
|
||||
result = self.search_for_thing(
|
||||
search_str,
|
||||
permitted_things,
|
||||
add_nonexisting,
|
||||
find_hidden_products,
|
||||
)
|
||||
num = 1
|
||||
else:
|
||||
result = self.search_for_thing(
|
||||
search_str, permitted_things, add_nonexisting, find_hidden_products
|
||||
)
|
||||
num = 1
|
||||
|
||||
if (result is None) and (len(search_lst) > 1):
|
||||
print('Interpreting input as "<number> <product>"')
|
||||
try:
|
||||
num = int(search_lst[0])
|
||||
result = self.search_for_thing(
|
||||
" ".join(search_lst[1:]),
|
||||
permitted_things,
|
||||
add_nonexisting,
|
||||
find_hidden_products,
|
||||
)
|
||||
# Her kan det legges inn en except ValueError,
|
||||
# men da blir det fort mye plaging av brukeren
|
||||
except Exception as e:
|
||||
print(e)
|
||||
if (result is None) and (len(search_lst) > 1):
|
||||
print('Interpreting input as "<number> <product>"')
|
||||
try:
|
||||
num = int(search_lst[0])
|
||||
result = self.search_for_thing(
|
||||
" ".join(search_lst[1:]),
|
||||
permitted_things,
|
||||
add_nonexisting,
|
||||
find_hidden_products,
|
||||
)
|
||||
# Her kan det legges inn en except ValueError,
|
||||
# men da blir det fort mye plaging av brukeren
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return result, num
|
||||
|
||||
def search_for_thing(
|
||||
self,
|
||||
search_str: str,
|
||||
permitted_things: Iterable[str] = ("user", "product"),
|
||||
add_non_existing: Iterable[str] = (),
|
||||
find_hidden_products: bool = True,
|
||||
) -> User | Product | None:
|
||||
search_fun = {
|
||||
"user": search_user,
|
||||
"product": search_product,
|
||||
}
|
||||
search_str,
|
||||
permitted_things=("user", "product"),
|
||||
add_non_existing=(),
|
||||
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]:
|
||||
@@ -414,14 +353,10 @@ class Menu:
|
||||
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):
|
||||
@@ -432,19 +367,18 @@ class Menu:
|
||||
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}"),
|
||||
@@ -453,14 +387,12 @@ class Menu:
|
||||
selection = selector.execute()
|
||||
if selection == "create":
|
||||
username = self.input_str(
|
||||
prompt="Username for new user (should be same as PVV username)",
|
||||
end_prompt=None,
|
||||
regex=User.name_re,
|
||||
length_range=(1, 10),
|
||||
"Username for new user (should be same as PVV username)",
|
||||
User.name_re,
|
||||
(1, 10),
|
||||
)
|
||||
assert username is not None
|
||||
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")
|
||||
@@ -473,21 +405,11 @@ class Menu:
|
||||
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:
|
||||
@@ -507,41 +429,25 @@ class Menu:
|
||||
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
|
||||
def general_help() -> None:
|
||||
def general_help():
|
||||
print(
|
||||
"""
|
||||
DIBBLER HELP
|
||||
@@ -564,10 +470,10 @@ class Menu:
|
||||
of money PVVVV owes the user. This value decreases with the
|
||||
appropriate amount when you register a purchase, and you may increase
|
||||
it by putting money in the box and using the "Adjust credit" menu.
|
||||
""",
|
||||
"""
|
||||
)
|
||||
|
||||
def local_help(self) -> None:
|
||||
def local_help(self):
|
||||
if self.help_text is None:
|
||||
print("no help here")
|
||||
else:
|
||||
@@ -575,15 +481,21 @@ class Menu:
|
||||
print(f"Help for {self.header()}:")
|
||||
print(self.help_text)
|
||||
|
||||
def execute(self, **_kwargs) -> MenuItemType | int | None:
|
||||
def execute(self, **kwargs):
|
||||
self.set_context(None)
|
||||
try:
|
||||
return self._execute(**_kwargs)
|
||||
except ExitMenuException:
|
||||
if self.uses_db and not self.session:
|
||||
self.session = Session()
|
||||
return self._execute(**kwargs)
|
||||
except ExitMenu:
|
||||
self.at_exit()
|
||||
return None
|
||||
finally:
|
||||
if self.session is not None:
|
||||
self.session.close()
|
||||
self.session = None
|
||||
|
||||
def _execute(self, **_kwargs) -> MenuItemType | int | None:
|
||||
def _execute(self, **kwargs):
|
||||
while True:
|
||||
self.print_header()
|
||||
self.set_context(None)
|
||||
@@ -602,21 +514,12 @@ class Menu:
|
||||
|
||||
|
||||
class MessageMenu(Menu):
|
||||
message: str
|
||||
pause_after_message: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
message: str,
|
||||
sql_session: Session,
|
||||
pause_after_message: bool = True,
|
||||
) -> None:
|
||||
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
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
print("")
|
||||
print(self.message)
|
||||
@@ -625,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,
|
||||
) -> None:
|
||||
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",
|
||||
@@ -643,55 +539,45 @@ class ConfirmMenu(Menu):
|
||||
self.default = default
|
||||
self.timeout = timeout
|
||||
|
||||
def _execute(self, **_kwargs) -> bool:
|
||||
options = {True: "[Y/n]", False: "[y/N]", None: "[y/n]"}[self.default]
|
||||
def _execute(self):
|
||||
options = {True: "[y]/n", False: "y/[n]", None: "y/n"}[self.default]
|
||||
while True:
|
||||
result = self.input_str(
|
||||
f"{self.prompt} {options}",
|
||||
end_prompt=": ",
|
||||
timeout=self.timeout,
|
||||
f"{self.prompt} ({options})", end_prompt=": ", timeout=self.timeout
|
||||
)
|
||||
result = result.lower().strip()
|
||||
if result in ["y", "yes"]:
|
||||
return True
|
||||
if result in ["n", "no"]:
|
||||
elif result in ["n", "no"]:
|
||||
return False
|
||||
if self.default is not None and result == "":
|
||||
elif self.default is not None and result == "":
|
||||
return self.default
|
||||
print("Please answer yes or no")
|
||||
else:
|
||||
print("Please answer yes or no")
|
||||
|
||||
|
||||
class Selector(Menu):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
sql_session: Session,
|
||||
items: list[Self | tuple[MenuItemType, str] | str] | None = None,
|
||||
prompt: str | None = "select",
|
||||
return_index: bool = True,
|
||||
exit_msg: str | None = None,
|
||||
exit_confirm_msg: str | None = None,
|
||||
help_text: str | None = None,
|
||||
) -> None:
|
||||
name,
|
||||
items=None,
|
||||
prompt="select",
|
||||
return_index=True,
|
||||
exit_msg=None,
|
||||
exit_confirm_msg=None,
|
||||
help_text=None,
|
||||
):
|
||||
if items is None:
|
||||
items = []
|
||||
super().__init__(
|
||||
name,
|
||||
sql_session,
|
||||
items,
|
||||
prompt,
|
||||
return_index=return_index,
|
||||
exit_msg=exit_msg,
|
||||
help_text=help_text,
|
||||
)
|
||||
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.")
|
||||
|
||||
+20
-16
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from dibbler.db import Session
|
||||
|
||||
from .buymenu import BuyMenu
|
||||
from .faq import FAQMenu
|
||||
@@ -12,17 +13,14 @@ faq_commands = ["faq"]
|
||||
restart_commands = ["restart"]
|
||||
|
||||
|
||||
def restart() -> None:
|
||||
def restart():
|
||||
# Does not work if the script is not executable, or if it was
|
||||
# started by searching $PATH.
|
||||
os.execv(sys.argv[0], sys.argv)
|
||||
|
||||
|
||||
class MainMenu(Menu):
|
||||
def __init__(self, sql_session: Session, **_kwargs) -> None:
|
||||
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])
|
||||
@@ -30,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)])
|
||||
@@ -38,26 +36,32 @@ 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?"):
|
||||
restart()
|
||||
pass
|
||||
return True
|
||||
if result == "c":
|
||||
print(f"\033[{random.randint(40, 49)};{random.randint(30, 37)};5m")
|
||||
print("\033[2J")
|
||||
elif result == "c":
|
||||
os.system(
|
||||
'echo -e "\033['
|
||||
+ str(random.randint(40, 49))
|
||||
+ ";"
|
||||
+ str(random.randint(30, 37))
|
||||
+ ';5m"'
|
||||
)
|
||||
os.system("clear")
|
||||
self.show_context()
|
||||
return True
|
||||
if result == "cs":
|
||||
print("\033[0m")
|
||||
print("\033[2J")
|
||||
elif result == "cs":
|
||||
os.system('echo -e "\033[0m"')
|
||||
os.system("clear")
|
||||
self.show_context()
|
||||
return True
|
||||
return False
|
||||
|
||||
def invalid_menu_choice(self, in_str: str) -> None:
|
||||
def invalid_menu_choice(self, in_str):
|
||||
print(self.show_context())
|
||||
|
||||
+46
-49
@@ -1,19 +1,17 @@
|
||||
import sqlalchemy
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.conf import config
|
||||
from dibbler.models import Transaction, Product, User
|
||||
from dibbler.lib.helpers import less
|
||||
from dibbler.models import Product, Transaction, User
|
||||
|
||||
from .helpermenus import Menu, Selector
|
||||
|
||||
|
||||
class TransferMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Transfer credit between users", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Transfer credit between users", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
amount = self.input_int("Transfer amount", 1, 100000)
|
||||
self.set_context(f"Transferring {amount:d} kr", display=False)
|
||||
@@ -28,25 +26,24 @@ 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")
|
||||
print(f"Comment: {comment}")
|
||||
except SQLAlchemyError as e:
|
||||
self.sql_session.rollback()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not perform transfer: {e}")
|
||||
# self.pause()
|
||||
|
||||
|
||||
class ShowUserMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Show user", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Show user", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
user = self.input_user("User name, card number or RFID")
|
||||
print(f"User name: {user.name}")
|
||||
@@ -55,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"),
|
||||
@@ -69,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":
|
||||
@@ -78,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
|
||||
@@ -91,7 +87,10 @@ class ShowUserMenu(Menu):
|
||||
if t.purchase:
|
||||
products = []
|
||||
for entry in t.purchase.entries:
|
||||
amount = f"{abs(entry.amount)}x " if abs(entry.amount) != 1 else ""
|
||||
if abs(entry.amount) != 1:
|
||||
amount = f"{abs(entry.amount)}x "
|
||||
else:
|
||||
amount = ""
|
||||
product = f"{amount}{entry.product.name}"
|
||||
products.append(product)
|
||||
string += "purchase ("
|
||||
@@ -99,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
|
||||
@@ -124,13 +123,13 @@ class ShowUserMenu(Menu):
|
||||
|
||||
|
||||
class UserListMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("User list", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "User list", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
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"
|
||||
@@ -145,10 +144,10 @@ class UserListMenu(Menu):
|
||||
|
||||
|
||||
class AdjustCreditMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Adjust credit", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Adjust credit", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
user = self.input_user("User")
|
||||
print(f"User {user.name}'s credit is {user.credit:d} kr")
|
||||
@@ -165,25 +164,24 @@ 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 SQLAlchemyError as e:
|
||||
self.sql_session.rollback()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f"Could not store transaction: {e}")
|
||||
# self.pause()
|
||||
|
||||
|
||||
class ProductListMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Product list", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Product list", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
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,22 +204,21 @@ class ProductListMenu(Menu):
|
||||
|
||||
|
||||
class ProductSearchMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Product search", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Product search", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
self.set_context("Enter (part of) product name or bar code")
|
||||
product = self.input_product()
|
||||
print(
|
||||
", ".join(
|
||||
[
|
||||
f"Result: {product.name}",
|
||||
f"price: {product.price} kr",
|
||||
f"bar code: {product.bar_code}",
|
||||
f"stock: {product.stock}",
|
||||
f"hidden: {'Y' if product.hidden else 'N'}",
|
||||
],
|
||||
),
|
||||
"Result: %s, price: %d kr, bar code: %s, stock: %d, hidden: %s"
|
||||
% (
|
||||
product.name,
|
||||
product.price,
|
||||
product.bar_code,
|
||||
product.stock,
|
||||
("Y" if product.hidden else "N"),
|
||||
)
|
||||
)
|
||||
# self.pause()
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
from sqlalchemy.orm import Session
|
||||
import re
|
||||
|
||||
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) -> None:
|
||||
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
|
||||
|
||||
Put it up somewhere in the vicinity.
|
||||
"""
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
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",
|
||||
)
|
||||
|
||||
+23
-28
@@ -1,9 +1,8 @@
|
||||
from sqlalchemy import desc, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dibbler.lib.helpers import less
|
||||
from dibbler.models import PurchaseEntry, Product, User
|
||||
from dibbler.lib.statistikkHelpers import statisticsTextOnly
|
||||
from dibbler.models import Product, PurchaseEntry, User
|
||||
|
||||
from .helpermenus import Menu
|
||||
|
||||
@@ -16,14 +15,14 @@ __all__ = [
|
||||
|
||||
|
||||
class ProductPopularityMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Products by popularity", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Products by popularity", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
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,8 +31,8 @@ class ProductPopularityMenu(Menu):
|
||||
.subquery()
|
||||
)
|
||||
product_list = (
|
||||
self.sql_session.query(Product, sub.c.purchase_count)
|
||||
.outerjoin(sub, Product.product_id == sub.c.product_id)
|
||||
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)
|
||||
.all()
|
||||
@@ -49,14 +48,14 @@ class ProductPopularityMenu(Menu):
|
||||
|
||||
|
||||
class ProductRevenueMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Products by revenue", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Products by revenue", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
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,8 +64,8 @@ class ProductRevenueMenu(Menu):
|
||||
.subquery()
|
||||
)
|
||||
product_list = (
|
||||
self.sql_session.query(Product, sub.c.purchase_count)
|
||||
.outerjoin(sub, Product.product_id == sub.c.product_id)
|
||||
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)
|
||||
.all()
|
||||
@@ -87,26 +86,22 @@ class ProductRevenueMenu(Menu):
|
||||
|
||||
|
||||
class BalanceMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Total balance of PVVVV", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Total balance of PVVVV", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
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.coalesce(func.sum(User.credit), 0))
|
||||
.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.coalesce(func.sum(User.credit), 0))
|
||||
.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
|
||||
@@ -124,8 +119,8 @@ class BalanceMenu(Menu):
|
||||
|
||||
|
||||
class LoggedStatisticsMenu(Menu):
|
||||
def __init__(self, sql_session: Session) -> None:
|
||||
super().__init__("Statistics from log", sql_session)
|
||||
def __init__(self):
|
||||
Menu.__init__(self, "Statistics from log", uses_db=True)
|
||||
|
||||
def _execute(self, **_kwargs) -> None:
|
||||
statisticsTextOnly(self.sql_session)
|
||||
def _execute(self):
|
||||
statisticsTextOnly()
|
||||
|
||||
@@ -18,7 +18,7 @@ class Base(DeclarativeBase):
|
||||
"ck": "ck_%(table_name)s_`%(constraint_name)s`",
|
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||
"pk": "pk_%(table_name)s",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@declared_attr.directive
|
||||
@@ -38,7 +38,7 @@ class Base(DeclarativeBase):
|
||||
isinstance(v, InstrumentedList),
|
||||
isinstance(v, InstrumentedSet),
|
||||
isinstance(v, InstrumentedDict),
|
||||
],
|
||||
]
|
||||
)
|
||||
)
|
||||
return f"<{self.__class__.__name__}({columns})>"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
@@ -37,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,
|
||||
) -> None:
|
||||
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
|
||||
|
||||
+14
-14
@@ -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,23 +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) -> None:
|
||||
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)))
|
||||
return int(math.floor(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
|
||||
@@ -53,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)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
ForeignKey,
|
||||
Integer,
|
||||
ForeignKey,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
@@ -28,15 +27,10 @@ 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: Purchase,
|
||||
product: Product,
|
||||
amount: int,
|
||||
) -> None:
|
||||
def __init__(self, purchase, product, amount):
|
||||
self.product = product
|
||||
self.product_bar_code = product.bar_code
|
||||
self.purchase = purchase
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
DateTime,
|
||||
@@ -18,8 +18,8 @@ from sqlalchemy.orm import (
|
||||
from .Base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .Purchase import Purchase
|
||||
from .User import User
|
||||
from .Purchase import Purchase
|
||||
|
||||
|
||||
class Transaction(Base):
|
||||
@@ -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,
|
||||
) -> None:
|
||||
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
|
||||
|
||||
+7
-17
@@ -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,
|
||||
) -> None:
|
||||
def __init__(self, name, card, rfid=None, credit=0):
|
||||
self.name = name
|
||||
if card == "":
|
||||
card = None
|
||||
@@ -52,8 +42,8 @@ class User(Base):
|
||||
self.rfid = rfid
|
||||
self.credit = credit
|
||||
|
||||
def __str__(self) -> str:
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def is_anonymous(self) -> bool:
|
||||
def is_anonymous(self):
|
||||
return self.card == "11122233"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
ForeignKey,
|
||||
Integer,
|
||||
ForeignKey,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
@@ -15,8 +14,8 @@ from sqlalchemy.orm import (
|
||||
from .Base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .Product import Product
|
||||
from .User import User
|
||||
from .Product import Product
|
||||
|
||||
|
||||
class UserProducts(Base):
|
||||
@@ -28,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
|
||||
|
||||
+38
-70
@@ -1,111 +1,79 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
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) -> None:
|
||||
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_menu = MainMenu(
|
||||
sql_session,
|
||||
main = MainMenu(
|
||||
"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"]:
|
||||
main_menu.exit_disallowed_msg = (
|
||||
"You can check out any time you like, but you can never leave."
|
||||
)
|
||||
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
|
||||
try:
|
||||
main_menu.execute()
|
||||
main.execute()
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
print("Interrupted.")
|
||||
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.")
|
||||
main_menu.sql_session.reset()
|
||||
|
||||
|
||||
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) -> None:
|
||||
def main():
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,46 +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) -> None:
|
||||
sql_session.query(Product).delete()
|
||||
sql_session.query(User).delete()
|
||||
sql_session.commit()
|
||||
|
||||
|
||||
def main(sql_session: Session) -> None:
|
||||
clear_db(sql_session)
|
||||
product_items = []
|
||||
user_items = []
|
||||
|
||||
with Path.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) -> None:
|
||||
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()
|
||||
|
||||
+199
-199
@@ -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()
|
||||
|
||||
@@ -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 = '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
|
||||
Generated
+274
-4
@@ -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": 1779560665,
|
||||
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
||||
"lastModified": 1730531603,
|
||||
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,73 +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;
|
||||
inherit (self) sourceInfo;
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
+71
-170
@@ -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,49 +0,0 @@
|
||||
{ lib
|
||||
, sourceInfo
|
||||
, python3Packages
|
||||
, makeWrapper
|
||||
, less
|
||||
}:
|
||||
let
|
||||
pyproject = builtins.fromTOML (builtins.readFile ../pyproject.toml);
|
||||
in
|
||||
python3Packages.buildPythonApplication {
|
||||
pname = pyproject.project.name;
|
||||
version = "0.1";
|
||||
src = lib.cleanSource ../.;
|
||||
|
||||
format = "pyproject";
|
||||
|
||||
# brother-ql is breaky breaky
|
||||
# https://github.com/NixOS/nixpkgs/issues/285234
|
||||
# dontCheckRuntimeDeps = true;
|
||||
|
||||
env.SETUPTOOLS_SCM_PRETEND_METADATA = (x: "{${x}}") (lib.concatStringsSep ", " [
|
||||
"node=\"${sourceInfo.rev or (lib.substring 0 64 sourceInfo.dirtyRev)}\""
|
||||
"node_date=${lib.substring 0 4 sourceInfo.lastModifiedDate}-${lib.substring 4 2 sourceInfo.lastModifiedDate}-${lib.substring 6 2 sourceInfo.lastModifiedDate}"
|
||||
"dirty=${if sourceInfo ? dirtyRev then "true" else "false"}"
|
||||
]);
|
||||
|
||||
nativeBuildInputs = with python3Packages; [
|
||||
makeWrapper
|
||||
setuptools
|
||||
setuptools-scm
|
||||
];
|
||||
propagatedBuildInputs = with python3Packages; [
|
||||
# brother-ql
|
||||
# matplotlib
|
||||
psycopg2-binary
|
||||
# python-barcode
|
||||
sqlalchemy
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
wrapProgram $out/bin/dibbler \
|
||||
--prefix PATH : "${lib.makeBinPath [ less ]}"
|
||||
'';
|
||||
|
||||
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
|
||||
]))
|
||||
];
|
||||
}
|
||||
+10
-47
@@ -1,71 +1,34 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "setuptools-scm"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = [
|
||||
"setuptools",
|
||||
"setuptools-scm",
|
||||
]
|
||||
|
||||
[project]
|
||||
name = "dibbler"
|
||||
dynamic = ["version"]
|
||||
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",
|
||||
]
|
||||
scripts.dibbler = "dibbler.main:main"
|
||||
dynamic = ["version"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["dibbler*"]
|
||||
|
||||
[tool.setuptools_scm]
|
||||
version_file = "dibbler/_version.py"
|
||||
[project.scripts]
|
||||
dibbler = "dibbler.main:main"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"A", # flake8-builtins
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"COM", # flake8-commas
|
||||
"ANN",
|
||||
# "E", # pycodestyle
|
||||
# "F", # Pyflakes
|
||||
"FA", # flake8-future-annotations
|
||||
"I", # isort
|
||||
"S", # flake8-bandit
|
||||
"ICN", # flake8-import-conventions
|
||||
"ISC", # flake8-implicit-str-concat
|
||||
# "N", # pep8-naming
|
||||
"PTH", # flake8-use-pathlib
|
||||
# "RET", # flake8-return
|
||||
# "SIM", # flake8-simplify
|
||||
"TC", # flake8-type-checking
|
||||
"UP", # pyupgrade
|
||||
"YTT", # flake8-2020
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long
|
||||
"S101", # assert detected
|
||||
"S311", # non-cryptographic random generator
|
||||
]
|
||||
|
||||
[tool.ruff.lint.flake8-annotations]
|
||||
suppress-dummy-args = true
|
||||
ignore-fully-untyped = true
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "dibbler"
|
||||
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.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", size = 284764, upload-time = "2026-05-20T13:09:10.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", size = 603479, upload-time = "2026-05-20T14:00:04.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", size = 615495, upload-time = "2026-05-20T14:05:40.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", size = 614754, upload-time = "2026-05-20T13:14:24.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", size = 1574097, upload-time = "2026-05-20T14:02:24.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", size = 1641058, upload-time = "2026-05-20T13:14:31.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/56/5f332b7705545eac2dc01b4e9254d24a793f2656d55d5cc6b94ee59d22ae/greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e", size = 238089, upload-time = "2026-05-20T13:14:03.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/a9/a3c2fa886c5b94863fb0e61b3bc14610b7aa94cf4f17f8741b11708305fc/greenlet-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:cc6ab7e555c8a112ad3a76e368e86e12a2754bcae1652a5602e133ec7b635523", size = 234989, upload-time = "2026-05-20T13:08:27.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580, upload-time = "2026-05-20T13:08:45.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/19/d4ce60954f3bb9d8e3bc5e5c4d1f2487de2d3851bf2391d54954c9df12a6/psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", size = 3712338, upload-time = "2026-04-20T23:34:03.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/71/c85409ee0d78890f0660eff262e815e7dd2bb741a17611d82e9e8cd9dc5e/psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", size = 3822407, upload-time = "2026-04-20T23:34:05.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/ed/60486c2c7f0d4d1ede2bfb1ed27e2498477ce646bc7f6b2759906303117e/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", size = 4578425, upload-time = "2026-04-20T23:34:08.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b9/656cb03fad9f4f49f2145c334b1126ee75189929ca4e6187d485a2d59951/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", size = 4273709, upload-time = "2026-04-20T23:34:10.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/66/08cf0da0e25cc6fb142c89be45fc8418792858f0c4cbff5e24530ff02cd6/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", size = 5893779, upload-time = "2026-04-20T23:34:13.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/d7/eecd9ce8e146d3721115d82d3836efdbb712187e4590325df549989d18f4/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", size = 4109308, upload-time = "2026-04-20T23:34:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2e/b1dc289b362cc8d45697b57eefbd673186f49a4ea0906928988e3affcc98/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", size = 3654405, upload-time = "2026-04-20T23:34:19.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/e4/4c4aea6473214dbdbd0fbba11aa4691e76dc01722c55724c5951719865ff/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", size = 3299187, upload-time = "2026-04-20T23:34:21.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5d/b03b99986446a4f57b170ed9a2579fb7ff9783ca0fa5226b19db99737fee/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", size = 3047716, upload-time = "2026-04-20T23:34:23.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/86/382ee4afbd1d97500c9d2862b20c2fdeddf4b7335e984df3fb4309f64108/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", size = 3349237, upload-time = "2026-04-20T23:34:25.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/16/9a57c75ba1eda7165c017342f526810d5f5a12647dde749c99ae9a7141d7/psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", size = 2757036, upload-time = "2026-04-20T23:34:27.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.50"
|
||||
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/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/5d/3172686af1770e4de2805f919a51441085f589ddadf3dd76ec582f84f497/sqlalchemy-2.0.50-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa6e403663a9c43c8fef7ce4bdb4cf48bcd8d352e91deda2a99f963270bd508", size = 2161366, upload-time = "2026-05-24T20:00:02.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/90/e98dedea3c3e663a17afcd003a34ba45efdac2cea3b6f2e4585e2b1e2537/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51b637a84f9fa35ae1f9017e786cb142974a25305085e1b378b3647a67f65ad3", size = 3318926, upload-time = "2026-05-24T20:07:42.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/4f/501308c2babb62c11753ecb4ee88ba9eef019419a4d6cbf7cb13e2bad353/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dab927761d9108550f0cf8e66ff21af56f907a0ce0a689793db615e2b55f62c", size = 3319199, upload-time = "2026-05-24T20:14:28.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/39/d88996c5e03ed6248c3a788d20f0b8d8b376b9f8a495e4bab9df7c72d2f8/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:545eae198d37bcf837a10ede3684e2af32458d6f35c597c35c2de7502dc38fc4", size = 3270301, upload-time = "2026-05-24T20:07:44.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/1b/1ae0e65161b51cc43e5ca75430ef79d80e23b5042d645586c2c342c3b92e/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fec460e18cdbb4c7773531122ce9a27e96c6ca17af3933941d94da475ad2c86", size = 3293465, upload-time = "2026-05-24T20:14:30.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/29/17c0003f2c0dfa6d1b97672475707e3ec5980db09defd7fa20beb6833bbd/sqlalchemy-2.0.50-cp311-cp311-win32.whl", hash = "sha256:e6e814658818fd165e749e3d8490ef16cc7f379a118c37ada8b0589ffbaaac22", size = 2120694, upload-time = "2026-05-24T20:08:09.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/18/280d00654cc19d1fccf236fa5070f6dd04b84dde6f1b2e637bde0ff340a7/sqlalchemy-2.0.50-cp311-cp311-win_amd64.whl", hash = "sha256:1c5f858fe79c9f5d8fda065c06186356acb7f8df3cd52dbd5ee3f200e4b144f5", size = 2145315, upload-time = "2026-05-24T20:08:10.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b0/a9d19b43f38f878b1278bca5b00b909f7540d41494396dd2561f9ad0956d/sqlalchemy-2.0.50-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb", size = 2159807, upload-time = "2026-05-24T19:27:53.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/2c/191dd58a248fd2cfd4780fa82c375c505e4ad98c8b522fa69ec492130d77/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89", size = 3343358, upload-time = "2026-05-24T20:09:29.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2b/514fce8a7df81cf5bad7ff7865de7ac0c5776a38cc043475c4703eb7fe8b/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600", size = 3357994, upload-time = "2026-05-24T20:17:13.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/a6/a0e283f5494f92b0d77e319ff77e437b1ffe4a051ba67c81d53234825475/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e", size = 3289399, upload-time = "2026-05-24T20:09:32.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/96/1b07325ba71752d6a028b77d07bed1483ad545f794e8b1dc89b3ba3b3c68/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615", size = 3321216, upload-time = "2026-05-24T20:17:15.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/8e/bad6ed253e8a99edfc99af02f7173ec48a1d3ed1b9b35a1b8bc1700900cc/sqlalchemy-2.0.50-cp312-cp312-win32.whl", hash = "sha256:1208050441471d003b7c8cb4054fb084f185cf35ac3f0ea270803865bca9939a", size = 2119194, upload-time = "2026-05-24T19:50:04.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2d/314a6690dda4b9cfc571eab1a63cf6fe6e1470aa3759ccda6aa016ee0f5a/sqlalchemy-2.0.50-cp312-cp312-win_amd64.whl", hash = "sha256:9d1af51558029a156a70986b7df88f042b3d158d7c8d8fb5072912d4b32d89c7", size = 2146186, upload-time = "2026-05-24T19:50:06.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" },
|
||||
]
|
||||
|
||||
[[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