Compare commits
68 Commits
restructur
...
worblehat
| Author | SHA1 | Date | |
|---|---|---|---|
| 25c604cbca | |||
| 6e0b207284 | |||
| fa51ede141 | |||
| e6d276e3ac | |||
| 8ad532878e | |||
| 978c5fbb51 | |||
| 55457fbeac | |||
| a4c4079324 | |||
|
fb0f24cb67
|
|||
|
3d555ca9d1
|
|||
|
af5710d663
|
|||
|
4d88409e97
|
|||
|
72cd066414
|
|||
|
b1bb1e556b
|
|||
|
70b04c0c45
|
|||
|
7bea5b0b96
|
|||
|
3123b8b474
|
|||
|
9091adedad
|
|||
|
94955cb706
|
|||
|
3b6cd1d354
|
|||
|
c2ee66c394
|
|||
|
b5b2706085
|
|||
|
bf9cea7dfc
|
|||
|
cf945143ba
|
|||
|
e84b43e2a0
|
|||
|
17fc23ba97
|
|||
|
45179a9c43
|
|||
|
dfaa818f46
|
|||
|
ec43f67e58
|
|||
|
1b09a904cb
|
|||
|
8e84669d9b
|
|||
|
1d01e1b2cb
|
|||
|
019f419b12
|
|||
|
3bab62b3ac
|
|||
|
e771fb0240
|
|||
|
2331e53795
|
|||
|
2ae651a1fa
|
|||
|
76f07841be
|
|||
|
ecaec99212
|
|||
|
cb385097dc
|
|||
|
b86962ef0e
|
|||
|
9c0bd54be6
|
|||
|
919d7a5afe
|
|||
|
ddca959ad6
|
|||
|
1733843b77
|
|||
|
4ed68ff05c
|
|||
|
78161a96be
|
|||
|
f4b5e1d6d4
|
|||
|
634716956e
|
|||
|
fb81eef26f
|
|||
|
e9d30b63a5
|
|||
| 0844843e59 | |||
|
70677f7f79
|
|||
|
4a4f0e6947
|
|||
| a4d10ad0c7 | |||
|
a654baba11
|
|||
| e69d04dcd0 | |||
| b2a6384f31 | |||
| 4f89765070 | |||
| 914e5b4e50 | |||
|
de20bad7dd
|
|||
|
4bab5e7e21
|
|||
|
b85a6535fe
|
|||
|
22a09b4177
|
|||
|
c39b15d1a8
|
|||
|
122ac2ab18
|
|||
|
28228beccd
|
|||
| 8a6a0c12ba |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,8 +1,13 @@
|
|||||||
result
|
result
|
||||||
result-*
|
result-*
|
||||||
|
**/__pycache__
|
||||||
|
dibbler.egg-info
|
||||||
dist
|
dist
|
||||||
|
|
||||||
test.db
|
test.db
|
||||||
|
|
||||||
.ruff_cache
|
.ruff_cache
|
||||||
|
|
||||||
|
*.qcow2
|
||||||
|
|
||||||
|
dibbler/_version.py
|
||||||
|
|||||||
59
README.md
59
README.md
@@ -2,30 +2,55 @@
|
|||||||
|
|
||||||
EDB-system for PVVVV
|
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
|
## Nix
|
||||||
### Hvordan kjøre
|
|
||||||
|
|
||||||
`nix run github:Prograrmvarverkstedet/dibbler`
|
> [!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.
|
||||||
|
|
||||||
|
Du kan enklest komme i gang med nix-utvikling ved å kjøre test VM-en:
|
||||||
|
|
||||||
### Bygge nytt image
|
```console
|
||||||
|
nix run .#vm
|
||||||
|
|
||||||
For å bygge et image trenger du en builder som takler å bygge for arkitekturen du skal lage et image for.
|
# Eller hvis du trenger tilgang til terminalen i VM-en også:
|
||||||
|
nix run .#vm-non-kiosk
|
||||||
|
```
|
||||||
|
|
||||||
(Eller be til gudene om at cross compile funker)
|
Du kan også bygge pakken manuelt, eller kjøre den direkte:
|
||||||
|
|
||||||
Flaket exposer en modul som autologger inn med en bruker som automatisk kjører dibbler, og setter opp et minimalistisk miljø.
|
```console
|
||||||
|
nix build .#dibbler
|
||||||
|
|
||||||
Før du bygger imaget burde du endre conf.py lokalt til å inneholde instillingene dine. **NB: Denne kommer til å ligge i nix storen.**
|
nix run .# -- --config example-config.toml create-db
|
||||||
|
nix run .# -- --config example-config.toml seed-data
|
||||||
|
nix run .# -- --config example-config.toml loop
|
||||||
|
```
|
||||||
|
|
||||||
Du kan også endre hvilken conf.py som blir brukt direkte i pakken eller i modulen.
|
## Produksjonssetting
|
||||||
|
|
||||||
Se eksempelet for hvordan skrot er satt opp i flake.nix
|
Se https://wiki.pvv.ntnu.no/wiki/Drift/Dibbler
|
||||||
|
|
||||||
### Bygge image for skrot
|
|
||||||
Skrot har et image definert i flake.nix:
|
|
||||||
|
|
||||||
1. endre conf.py
|
|
||||||
2. `nix build .#images.skrot`
|
|
||||||
3. ???
|
|
||||||
4. non-profit
|
|
||||||
|
|||||||
13
conf.py
13
conf.py
@@ -1,13 +0,0 @@
|
|||||||
db_url = "postgresql://robertem@127.0.0.1/pvvvv"
|
|
||||||
quit_allowed = True
|
|
||||||
stop_allowed = False
|
|
||||||
show_tracebacks = True
|
|
||||||
input_encoding = "utf8"
|
|
||||||
|
|
||||||
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
|
|
||||||
label_type = "62"
|
|
||||||
label_rotate = False
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{ pkgs ? import <nixos-unstable> { } }:
|
|
||||||
{
|
|
||||||
dibbler = pkgs.callPackage ./nix/dibbler.nix { };
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,56 @@
|
|||||||
# This module is supposed to act as a singleton and be filled
|
import os
|
||||||
# with config variables by cli.py
|
import sys
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import configparser
|
from dibbler.lib.helpers import file_is_submissive_and_readable
|
||||||
|
|
||||||
config = configparser.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)
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
from dibbler.conf import config
|
|
||||||
|
|
||||||
engine = create_engine(config.get("database", "url"))
|
|
||||||
Session = sessionmaker(bind=engine)
|
|
||||||
@@ -1,70 +1,71 @@
|
|||||||
import os
|
# import os
|
||||||
|
|
||||||
from PIL import ImageFont
|
# from PIL import ImageFont
|
||||||
from barcode.writer import ImageWriter, mm2px
|
# from barcode.writer import ImageWriter, mm2px
|
||||||
from brother_ql.devicedependent import label_type_specs
|
# from brother_ql.labels import ALL_LABELS
|
||||||
|
|
||||||
|
|
||||||
def px2mm(px, dpi=300):
|
# def px2mm(px, dpi=300):
|
||||||
return (25.4 * px) / dpi
|
# return (25.4 * px) / dpi
|
||||||
|
|
||||||
|
|
||||||
class BrotherLabelWriter(ImageWriter):
|
# class BrotherLabelWriter(ImageWriter):
|
||||||
def __init__(self, typ="62", max_height=350, rot=False, text=None):
|
# def __init__(self, typ="62", max_height=350, rot=False, text=None):
|
||||||
super(BrotherLabelWriter, self).__init__()
|
# super(BrotherLabelWriter, self).__init__()
|
||||||
assert typ in label_type_specs
|
# label = next([l for l in ALL_LABELS if l.identifier == typ])
|
||||||
self.rot = rot
|
# assert label is not None
|
||||||
if self.rot:
|
# self.rot = rot
|
||||||
self._h, self._w = label_type_specs[typ]["dots_printable"]
|
# if self.rot:
|
||||||
if self._w == 0 or self._w > max_height:
|
# self._h, self._w = label.dots_printable
|
||||||
self._w = min(max_height, self._h / 2)
|
# if self._w == 0 or self._w > max_height:
|
||||||
else:
|
# self._w = min(max_height, self._h / 2)
|
||||||
self._w, self._h = label_type_specs[typ]["dots_printable"]
|
# else:
|
||||||
if self._h == 0 or self._h > max_height:
|
# self._w, self._h = label.dots_printable
|
||||||
self._h = min(max_height, self._w / 2)
|
# if self._h == 0 or self._h > max_height:
|
||||||
self._xo = 0.0
|
# self._h = min(max_height, self._w / 2)
|
||||||
self._yo = 0.0
|
# self._xo = 0.0
|
||||||
self._title = text
|
# self._yo = 0.0
|
||||||
|
# self._title = text
|
||||||
|
|
||||||
def _init(self, code):
|
# def _init(self, code):
|
||||||
self.text = None
|
# self.text = None
|
||||||
super(BrotherLabelWriter, self)._init(code)
|
# super(BrotherLabelWriter, self)._init(code)
|
||||||
|
|
||||||
def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
|
# def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
|
||||||
x, y = super(BrotherLabelWriter, self).calculate_size(
|
# x, y = super(BrotherLabelWriter, self).calculate_size(
|
||||||
modules_per_line, number_of_lines, dpi
|
# modules_per_line, number_of_lines, dpi
|
||||||
)
|
# )
|
||||||
|
|
||||||
self._xo = (px2mm(self._w) - px2mm(x)) / 2
|
# self._xo = (px2mm(self._w) - px2mm(x)) / 2
|
||||||
self._yo = px2mm(self._h) - px2mm(y)
|
# self._yo = px2mm(self._h) - px2mm(y)
|
||||||
assert self._xo >= 0
|
# assert self._xo >= 0
|
||||||
assert self._yo >= 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):
|
# def _paint_module(self, xpos, ypos, width, color):
|
||||||
super(BrotherLabelWriter, self)._paint_module(
|
# super(BrotherLabelWriter, self)._paint_module(
|
||||||
xpos + self._xo, ypos + self._yo, width, color
|
# xpos + self._xo, ypos + self._yo, width, color
|
||||||
)
|
# )
|
||||||
|
|
||||||
def _paint_text(self, xpos, ypos):
|
# def _paint_text(self, xpos, ypos):
|
||||||
super(BrotherLabelWriter, self)._paint_text(xpos + self._xo, ypos + self._yo)
|
# super(BrotherLabelWriter, self)._paint_text(xpos + self._xo, ypos + self._yo)
|
||||||
|
|
||||||
def _finish(self):
|
# def _finish(self):
|
||||||
if self._title:
|
# if self._title:
|
||||||
width = self._w + 1
|
# width = self._w + 1
|
||||||
height = 0
|
# height = 0
|
||||||
max_h = self._h - mm2px(self._yo, self.dpi)
|
# max_h = self._h - mm2px(self._yo, self.dpi)
|
||||||
fs = int(max_h / 1.2)
|
# fs = int(max_h / 1.2)
|
||||||
font_path = os.path.join(
|
# font_path = os.path.join(
|
||||||
os.path.dirname(os.path.realpath(__file__)),
|
# os.path.dirname(os.path.realpath(__file__)),
|
||||||
"Stranger back in the Night.ttf",
|
# "Stranger back in the Night.ttf",
|
||||||
)
|
# )
|
||||||
font = ImageFont.truetype(font_path, 10)
|
# font = ImageFont.truetype(font_path, 10)
|
||||||
while width > self._w or height > max_h:
|
# while width > self._w or height > max_h:
|
||||||
font = ImageFont.truetype(font_path, fs)
|
# font = ImageFont.truetype(font_path, fs)
|
||||||
width, height = font.getsize(self._title)
|
# width, height = font.getsize(self._title)
|
||||||
fs -= 1
|
# fs -= 1
|
||||||
pos = ((self._w - width) // 2, 0 - (height // 8))
|
# pos = ((self._w - width) // 2, 0 - (height // 8))
|
||||||
self._draw.text(pos, self._title, font=font, fill=self.foreground)
|
# self._draw.text(pos, self._title, font=font, fill=self.foreground)
|
||||||
return self._image
|
# return self._image
|
||||||
|
|||||||
108
dibbler/lib/check_db_health.py
Normal file
108
dibbler/lib/check_db_health.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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)
|
||||||
@@ -1,51 +1,69 @@
|
|||||||
import pwd
|
|
||||||
import subprocess
|
|
||||||
import os
|
import os
|
||||||
|
import pwd
|
||||||
import signal
|
import signal
|
||||||
|
import subprocess
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
from sqlalchemy import or_, and_
|
from sqlalchemy import and_, not_, or_
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..models import User, Product
|
from ..models import Product, User
|
||||||
|
|
||||||
|
|
||||||
def search_user(string, session, ignorethisflag=None):
|
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
|
||||||
string = string.lower()
|
string = string.lower()
|
||||||
exact_match = (
|
exact_match = (
|
||||||
session.query(User)
|
sql_session.query(User)
|
||||||
.filter(or_(User.name == string, User.card == string, User.rfid == string))
|
.filter(or_(User.name == string, User.card == string, User.rfid == string))
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
if exact_match:
|
if exact_match:
|
||||||
return exact_match
|
return exact_match
|
||||||
user_list = (
|
return (
|
||||||
session.query(User)
|
sql_session.query(User)
|
||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
User.name.ilike(f"%{string}%"),
|
User.name.ilike(f"%{string}%"),
|
||||||
User.card.ilike(f"%{string}%"),
|
User.card.ilike(f"%{string}%"),
|
||||||
User.rfid.ilike(f"%{string}%"),
|
User.rfid.ilike(f"%{string}%"),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return user_list
|
|
||||||
|
|
||||||
|
|
||||||
def search_product(string, session, find_hidden_products=True):
|
def search_product(
|
||||||
|
string: str,
|
||||||
|
sql_session: Session,
|
||||||
|
find_hidden_products: bool = True,
|
||||||
|
) -> Product | list[Product] | None:
|
||||||
|
assert sql_session is not None
|
||||||
if find_hidden_products:
|
if find_hidden_products:
|
||||||
exact_match = (
|
exact_match = (
|
||||||
session.query(Product)
|
sql_session.query(Product)
|
||||||
.filter(or_(Product.bar_code == string, Product.name == string))
|
.filter(or_(Product.bar_code == string, Product.name == string))
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
exact_match = (
|
exact_match = (
|
||||||
session.query(Product)
|
sql_session.query(Product)
|
||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
Product.bar_code == string,
|
Product.bar_code == string,
|
||||||
and_(Product.name == string, Product.hidden is False),
|
and_(
|
||||||
)
|
Product.name == string,
|
||||||
|
not_(Product.hidden),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
@@ -53,30 +71,33 @@ def search_product(string, session, find_hidden_products=True):
|
|||||||
return exact_match
|
return exact_match
|
||||||
if find_hidden_products:
|
if find_hidden_products:
|
||||||
product_list = (
|
product_list = (
|
||||||
session.query(Product)
|
sql_session.query(Product)
|
||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
Product.bar_code.ilike(f"%{string}%"),
|
Product.bar_code.ilike(f"%{string}%"),
|
||||||
Product.name.ilike(f"%{string}%"),
|
Product.name.ilike(f"%{string}%"),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
product_list = (
|
product_list = (
|
||||||
session.query(Product)
|
sql_session.query(Product)
|
||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
Product.bar_code.ilike(f"%{string}%"),
|
Product.bar_code.ilike(f"%{string}%"),
|
||||||
and_(Product.name.ilike(f"%{string}%"), Product.hidden is False),
|
and_(
|
||||||
)
|
Product.name.ilike(f"%{string}%"),
|
||||||
|
not_(Product.hidden),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return product_list
|
return product_list
|
||||||
|
|
||||||
|
|
||||||
def system_user_exists(username):
|
def system_user_exists(username: str) -> bool:
|
||||||
try:
|
try:
|
||||||
pwd.getpwnam(username)
|
pwd.getpwnam(username)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -87,7 +108,7 @@ def system_user_exists(username):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def guess_data_type(string):
|
def guess_data_type(string: str) -> Literal["card", "rfid", "bar_code", "username"] | None:
|
||||||
if string.startswith("ntnu") and string[4:].isdigit():
|
if string.startswith("ntnu") and string[4:].isdigit():
|
||||||
return "card"
|
return "card"
|
||||||
if string.isdigit() and len(string) == 10:
|
if string.isdigit() and len(string) == 10:
|
||||||
@@ -101,7 +122,11 @@ def guess_data_type(string):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def argmax(d, all=False, value=None):
|
def argmax(
|
||||||
|
d: dict[Any, Any],
|
||||||
|
all_: bool = False,
|
||||||
|
value: Callable[[Any], Any] | None = None,
|
||||||
|
) -> Any | list[Any] | None:
|
||||||
maxarg = None
|
maxarg = None
|
||||||
if value is not None:
|
if value is not None:
|
||||||
dd = d
|
dd = d
|
||||||
@@ -111,12 +136,12 @@ def argmax(d, all=False, value=None):
|
|||||||
for key in list(d.keys()):
|
for key in list(d.keys()):
|
||||||
if maxarg is None or d[key] > d[maxarg]:
|
if maxarg is None or d[key] > d[maxarg]:
|
||||||
maxarg = key
|
maxarg = key
|
||||||
if all:
|
if all_:
|
||||||
return [k for k in list(d.keys()) if d[k] == d[maxarg]]
|
return [k for k in list(d.keys()) if d[k] == d[maxarg]]
|
||||||
return maxarg
|
return maxarg
|
||||||
|
|
||||||
|
|
||||||
def less(string):
|
def less(string: str) -> None:
|
||||||
"""
|
"""
|
||||||
Run less with string as input; wait until it finishes.
|
Run less with string as input; wait until it finishes.
|
||||||
"""
|
"""
|
||||||
@@ -128,3 +153,13 @@ def less(string):
|
|||||||
proc = subprocess.Popen("less", env=env, encoding="utf-8", stdin=subprocess.PIPE)
|
proc = subprocess.Popen("less", env=env, encoding="utf-8", stdin=subprocess.PIPE)
|
||||||
proc.communicate(string)
|
proc.communicate(string)
|
||||||
signal.signal(signal.SIGINT, int_handler)
|
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,96 +1,95 @@
|
|||||||
import os
|
# import barcode
|
||||||
import datetime
|
# from brother_ql.brother_ql_create import create_label
|
||||||
|
# from brother_ql.raster import BrotherQLRaster
|
||||||
|
# from brother_ql.backends import backend_factory
|
||||||
|
# from brother_ql.labels import ALL_LABELS
|
||||||
|
# from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
import barcode
|
# from .barcode_helpers import BrotherLabelWriter
|
||||||
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(
|
# def print_name_label(
|
||||||
text,
|
# text,
|
||||||
margin=10,
|
# margin=10,
|
||||||
rotate=False,
|
# rotate=False,
|
||||||
label_type="62",
|
# label_type="62",
|
||||||
printer_type="QL-700",
|
# printer_type="QL-700",
|
||||||
):
|
# ):
|
||||||
if not rotate:
|
# label = next([l for l in ALL_LABELS if l.identifier == label_type])
|
||||||
width, height = label_type_specs[label_type]["dots_printable"]
|
# if not rotate:
|
||||||
else:
|
# width, height = label.dots_printable
|
||||||
height, width = label_type_specs[label_type]["dots_printable"]
|
# else:
|
||||||
|
# height, width = label.dots_printable
|
||||||
|
|
||||||
font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ChopinScript.ttf")
|
# font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ChopinScript.ttf")
|
||||||
fs = 2000
|
# fs = 2000
|
||||||
tw, th = width, height
|
# tw, th = width, height
|
||||||
if width == 0:
|
# if width == 0:
|
||||||
while th + 2 * margin > height:
|
# while th + 2 * margin > height:
|
||||||
font = ImageFont.truetype(font_path, fs)
|
# font = ImageFont.truetype(font_path, fs)
|
||||||
tw, th = font.getsize(text)
|
# tw, th = font.getsize(text)
|
||||||
fs -= 1
|
# fs -= 1
|
||||||
width = tw + 2 * margin
|
# width = tw + 2 * margin
|
||||||
elif height == 0:
|
# elif height == 0:
|
||||||
while tw + 2 * margin > width:
|
# while tw + 2 * margin > width:
|
||||||
font = ImageFont.truetype(font_path, fs)
|
# font = ImageFont.truetype(font_path, fs)
|
||||||
tw, th = font.getsize(text)
|
# tw, th = font.getsize(text)
|
||||||
fs -= 1
|
# fs -= 1
|
||||||
height = th + 2 * margin
|
# height = th + 2 * margin
|
||||||
else:
|
# else:
|
||||||
while tw + 2 * margin > width or th + 2 * margin > height:
|
# while tw + 2 * margin > width or th + 2 * margin > height:
|
||||||
font = ImageFont.truetype(font_path, fs)
|
# font = ImageFont.truetype(font_path, fs)
|
||||||
tw, th = font.getsize(text)
|
# tw, th = font.getsize(text)
|
||||||
fs -= 1
|
# fs -= 1
|
||||||
|
|
||||||
xp = (width // 2) - (tw // 2)
|
# xp = (width // 2) - (tw // 2)
|
||||||
yp = (height // 2) - (th // 2)
|
# yp = (height // 2) - (th // 2)
|
||||||
|
|
||||||
im = Image.new("RGB", (width, height), (255, 255, 255))
|
# im = Image.new("RGB", (width, height), (255, 255, 255))
|
||||||
dr = ImageDraw.Draw(im)
|
# dr = ImageDraw.Draw(im)
|
||||||
|
|
||||||
dr.text((xp, yp), text, fill=(0, 0, 0), font=font)
|
# dr.text((xp, yp), text, fill=(0, 0, 0), font=font)
|
||||||
now = datetime.datetime.now()
|
# now = datetime.datetime.now()
|
||||||
date = now.strftime("%Y-%m-%d")
|
# date = now.strftime("%Y-%m-%d")
|
||||||
dr.text((0, 0), date, fill=(0, 0, 0))
|
# dr.text((0, 0), date, fill=(0, 0, 0))
|
||||||
|
|
||||||
base_path = os.path.dirname(os.path.realpath(__file__))
|
# base_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
fn = os.path.join(base_path, "bar_codes", text + ".png")
|
# fn = os.path.join(base_path, "bar_codes", text + ".png")
|
||||||
|
|
||||||
im.save(fn, "PNG")
|
# im.save(fn, "PNG")
|
||||||
print_image(fn, printer_type, label_type)
|
# print_image(fn, printer_type, label_type)
|
||||||
|
|
||||||
|
|
||||||
def print_bar_code(
|
# def print_bar_code(
|
||||||
barcode_value,
|
# barcode_value,
|
||||||
barcode_text,
|
# barcode_text,
|
||||||
barcode_type="ean13",
|
# barcode_type="ean13",
|
||||||
rotate=False,
|
# rotate=False,
|
||||||
printer_type="QL-700",
|
# printer_type="QL-700",
|
||||||
label_type="62",
|
# label_type="62",
|
||||||
):
|
# ):
|
||||||
bar_coder = barcode.get_barcode_class(barcode_type)
|
# bar_coder = barcode.get_barcode_class(barcode_type)
|
||||||
wr = BrotherLabelWriter(typ=label_type, rot=rotate, text=barcode_text, max_height=1000)
|
# wr = BrotherLabelWriter(typ=label_type, rot=rotate, text=barcode_text, max_height=1000)
|
||||||
|
|
||||||
test = bar_coder(barcode_value, writer=wr)
|
# test = bar_coder(barcode_value, writer=wr)
|
||||||
base_path = os.path.dirname(os.path.realpath(__file__))
|
# base_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
fn = test.save(os.path.join(base_path, "bar_codes", barcode_value))
|
# fn = test.save(os.path.join(base_path, "bar_codes", barcode_value))
|
||||||
print_image(fn, printer_type, label_type)
|
# print_image(fn, printer_type, label_type)
|
||||||
|
|
||||||
|
|
||||||
def print_image(fn, printer_type="QL-700", label_type="62"):
|
# def print_image(fn, printer_type="QL-700", label_type="62"):
|
||||||
qlr = BrotherQLRaster(printer_type)
|
# qlr = BrotherQLRaster(printer_type)
|
||||||
qlr.exception_on_warning = True
|
# qlr.exception_on_warning = True
|
||||||
create_label(qlr, fn, label_type, threshold=70, cut=True)
|
# create_label(qlr, fn, label_type, threshold=70, cut=True)
|
||||||
|
|
||||||
be = backend_factory("pyusb")
|
# be = backend_factory("pyusb")
|
||||||
list_available_devices = be["list_available_devices"]
|
# list_available_devices = be["list_available_devices"]
|
||||||
BrotherQLBackend = be["backend_class"]
|
# BrotherQLBackend = be["backend_class"]
|
||||||
|
|
||||||
ad = list_available_devices()
|
# ad = list_available_devices()
|
||||||
assert ad
|
# assert ad
|
||||||
string_descr = ad[0]["string_descr"]
|
# string_descr = ad[0]["string_descr"]
|
||||||
|
|
||||||
printer = BrotherQLBackend(string_descr)
|
# printer = BrotherQLBackend(string_descr)
|
||||||
|
|
||||||
printer.write(qlr.data)
|
# printer.write(qlr.data)
|
||||||
|
|||||||
@@ -3,18 +3,20 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .helpers import *
|
|
||||||
from ..models import Transaction
|
from ..models import Transaction
|
||||||
from ..db import Session
|
from .helpers import *
|
||||||
|
|
||||||
|
|
||||||
def getUser():
|
def getUser(sql_session: Session) -> str:
|
||||||
|
assert sql_session is not None
|
||||||
while 1:
|
while 1:
|
||||||
string = input("user? ")
|
string = input("user? ")
|
||||||
session = Session()
|
user = search_user(string, sql_session)
|
||||||
user = search_user(string, session)
|
sql_session.close()
|
||||||
session.close()
|
|
||||||
if not isinstance(user, list):
|
if not isinstance(user, list):
|
||||||
return user.name
|
return user.name
|
||||||
i = 0
|
i = 0
|
||||||
@@ -37,12 +39,11 @@ def getUser():
|
|||||||
return user[n].name
|
return user[n].name
|
||||||
|
|
||||||
|
|
||||||
def getProduct():
|
def getProduct(sql_session: Session) -> str:
|
||||||
|
assert sql_session is not None
|
||||||
while 1:
|
while 1:
|
||||||
string = input("product? ")
|
string = input("product? ")
|
||||||
session = Session()
|
product = search_product(string, sql_session)
|
||||||
product = search_product(string, session)
|
|
||||||
session.close()
|
|
||||||
if not isinstance(product, list):
|
if not isinstance(product, list):
|
||||||
return product.name
|
return product.name
|
||||||
i = 0
|
i = 0
|
||||||
@@ -76,12 +77,8 @@ class Database:
|
|||||||
personDatoVerdi = defaultdict(list) # dict->array
|
personDatoVerdi = defaultdict(list) # dict->array
|
||||||
personUkedagVerdi = defaultdict(list)
|
personUkedagVerdi = defaultdict(list)
|
||||||
# for global
|
# for global
|
||||||
personPosTransactions = (
|
personPosTransactions = {} # personPosTransactions[trygvrad] == 100 #trygvrad har lagt 100kr i boksen
|
||||||
{}
|
personNegTransactions = {} # personNegTransactions[trygvrad» == 70 #trygvrad har tatt 70kr fra boksen
|
||||||
) # personPosTransactions[trygvrad] == 100 #trygvrad har lagt 100kr i boksen
|
|
||||||
personNegTransactions = (
|
|
||||||
{}
|
|
||||||
) # personNegTransactions[trygvrad» == 70 #trygvrad har tatt 70kr fra boksen
|
|
||||||
globalVareAntall = {} # globalVareAntall[Oreo] == 3
|
globalVareAntall = {} # globalVareAntall[Oreo] == 3
|
||||||
globalVareVerdi = {} # globalVareVerdi[Oreo] == 30 #[kr]
|
globalVareVerdi = {} # globalVareVerdi[Oreo] == 30 #[kr]
|
||||||
globalPersonAntall = {} # globalPersonAntall[trygvrad] == 3
|
globalPersonAntall = {} # globalPersonAntall[trygvrad] == 3
|
||||||
@@ -93,7 +90,7 @@ class Database:
|
|||||||
|
|
||||||
|
|
||||||
class InputLine:
|
class InputLine:
|
||||||
def __init__(self, u, p, t):
|
def __init__(self, u, p, t) -> None:
|
||||||
self.inputUser = u
|
self.inputUser = u
|
||||||
self.inputProduct = p
|
self.inputProduct = p
|
||||||
self.inputType = t
|
self.inputType = t
|
||||||
@@ -126,17 +123,17 @@ def getInputType():
|
|||||||
return int(inp)
|
return int(inp)
|
||||||
|
|
||||||
|
|
||||||
def getProducts(products):
|
def getProducts(products: str) -> list[tuple[str]]:
|
||||||
product = []
|
product = []
|
||||||
products = products.partition("¤")
|
split_products = products.partition("¤")
|
||||||
product.append(products[0])
|
product.append(products[0])
|
||||||
while products[1] == "¤":
|
while products[1] == "¤":
|
||||||
products = products[2].partition("¤")
|
split_products = split_products[2].partition("¤")
|
||||||
product.append(products[0])
|
product.append(products[0])
|
||||||
return product
|
return product
|
||||||
|
|
||||||
|
|
||||||
def getDateFile(date, inp):
|
def getDateFile(date: str, inp: str) -> datetime.date:
|
||||||
try:
|
try:
|
||||||
year = inp.partition("-")
|
year = inp.partition("-")
|
||||||
month = year[2].partition("-")
|
month = year[2].partition("-")
|
||||||
@@ -180,7 +177,7 @@ def addLineToDatabase(database, inputLine):
|
|||||||
if abs(inputLine.price) > 90000:
|
if abs(inputLine.price) > 90000:
|
||||||
return database
|
return database
|
||||||
# fyller inn for varer
|
# fyller inn for varer
|
||||||
if (not inputLine.product == "") and (
|
if (inputLine.product != "") and (
|
||||||
(inputLine.inputProduct == "") or (inputLine.inputProduct == inputLine.product)
|
(inputLine.inputProduct == "") or (inputLine.inputProduct == inputLine.product)
|
||||||
):
|
):
|
||||||
database.varePersonAntall[inputLine.product][inputLine.user] = (
|
database.varePersonAntall[inputLine.product][inputLine.user] = (
|
||||||
@@ -194,7 +191,7 @@ def addLineToDatabase(database, inputLine):
|
|||||||
database.vareUkedagAntall[inputLine.product][inputLine.weekday] += 1
|
database.vareUkedagAntall[inputLine.product][inputLine.weekday] += 1
|
||||||
# fyller inn for personer
|
# fyller inn for personer
|
||||||
if (inputLine.inputUser == "") or (inputLine.inputUser == inputLine.user):
|
if (inputLine.inputUser == "") or (inputLine.inputUser == inputLine.user):
|
||||||
if not inputLine.product == "":
|
if inputLine.product != "":
|
||||||
database.personVareAntall[inputLine.user][inputLine.product] = (
|
database.personVareAntall[inputLine.user][inputLine.product] = (
|
||||||
database.personVareAntall[inputLine.user].setdefault(inputLine.product, 0) + 1
|
database.personVareAntall[inputLine.user].setdefault(inputLine.product, 0) + 1
|
||||||
)
|
)
|
||||||
@@ -218,7 +215,7 @@ def addLineToDatabase(database, inputLine):
|
|||||||
database.personNegTransactions[inputLine.user] = (
|
database.personNegTransactions[inputLine.user] = (
|
||||||
database.personNegTransactions.setdefault(inputLine.user, 0) + inputLine.price
|
database.personNegTransactions.setdefault(inputLine.user, 0) + inputLine.price
|
||||||
)
|
)
|
||||||
elif not (inputLine.inputType == 1):
|
elif inputLine.inputType != 1:
|
||||||
database.globalVareAntall[inputLine.product] = (
|
database.globalVareAntall[inputLine.product] = (
|
||||||
database.globalVareAntall.setdefault(inputLine.product, 0) + 1
|
database.globalVareAntall.setdefault(inputLine.product, 0) + 1
|
||||||
)
|
)
|
||||||
@@ -229,7 +226,7 @@ def addLineToDatabase(database, inputLine):
|
|||||||
# fyller inn for global statistikk
|
# fyller inn for global statistikk
|
||||||
if (inputLine.inputType == 3) or (inputLine.inputType == 4):
|
if (inputLine.inputType == 3) or (inputLine.inputType == 4):
|
||||||
database.pengebeholdning[inputLine.dateNum] += inputLine.price
|
database.pengebeholdning[inputLine.dateNum] += inputLine.price
|
||||||
if not (inputLine.product == ""):
|
if inputLine.product != "":
|
||||||
database.globalPersonAntall[inputLine.user] = (
|
database.globalPersonAntall[inputLine.user] = (
|
||||||
database.globalPersonAntall.setdefault(inputLine.user, 0) + 1
|
database.globalPersonAntall.setdefault(inputLine.user, 0) + 1
|
||||||
)
|
)
|
||||||
@@ -242,12 +239,12 @@ def addLineToDatabase(database, inputLine):
|
|||||||
return database
|
return database
|
||||||
|
|
||||||
|
|
||||||
def buildDatabaseFromDb(inputType, inputProduct, inputUser):
|
def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session):
|
||||||
|
assert sql_session is not None
|
||||||
sdate = input("enter start date (yyyy-mm-dd)? ")
|
sdate = input("enter start date (yyyy-mm-dd)? ")
|
||||||
edate = input("enter end date (yyyy-mm-dd)? ")
|
edate = input("enter end date (yyyy-mm-dd)? ")
|
||||||
print("building database...")
|
print("building database...")
|
||||||
session = Session()
|
transaction_list = sql_session.query(Transaction).all()
|
||||||
transaction_list = session.query(Transaction).all()
|
|
||||||
inputLine = InputLine(inputUser, inputProduct, inputType)
|
inputLine = InputLine(inputUser, inputProduct, inputType)
|
||||||
startDate = getDateDb(transaction_list[0].time, sdate)
|
startDate = getDateDb(transaction_list[0].time, sdate)
|
||||||
endDate = getDateDb(transaction_list[-1].time, edate)
|
endDate = getDateDb(transaction_list[-1].time, edate)
|
||||||
@@ -277,9 +274,9 @@ def buildDatabaseFromDb(inputType, inputProduct, inputUser):
|
|||||||
inputLine.price = 0
|
inputLine.price = 0
|
||||||
|
|
||||||
print("saving as default.dibblerlog...", end=" ")
|
print("saving as default.dibblerlog...", end=" ")
|
||||||
f = open("default.dibblerlog", "w")
|
f = Path.open("default.dibblerlog", "w")
|
||||||
line_format = "%s|%s|%s|%s|%s|%s\n"
|
line_format = "%s|%s|%s|%s|%s|%s\n"
|
||||||
transaction_list = session.query(Transaction).all()
|
transaction_list = sql_session.query(Transaction).all()
|
||||||
for transaction in transaction_list:
|
for transaction in transaction_list:
|
||||||
if transaction.purchase:
|
if transaction.purchase:
|
||||||
products = "¤".join([ent.product.name for ent in transaction.purchase.entries])
|
products = "¤".join([ent.product.name for ent in transaction.purchase.entries])
|
||||||
@@ -294,8 +291,7 @@ def buildDatabaseFromDb(inputType, inputProduct, inputUser):
|
|||||||
transaction.description,
|
transaction.description,
|
||||||
)
|
)
|
||||||
f.write(line.encode("utf8"))
|
f.write(line.encode("utf8"))
|
||||||
session.close()
|
f.close()
|
||||||
f.close
|
|
||||||
# bygg database.pengebeholdning
|
# bygg database.pengebeholdning
|
||||||
if (inputType == 3) or (inputType == 4):
|
if (inputType == 3) or (inputType == 4):
|
||||||
for i in range(inputLine.numberOfDays + 1):
|
for i in range(inputLine.numberOfDays + 1):
|
||||||
@@ -315,7 +311,7 @@ def buildDatabaseFromFile(inputFile, inputType, inputProduct, inputUser):
|
|||||||
sdate = input("enter start date (yyyy-mm-dd)? ")
|
sdate = input("enter start date (yyyy-mm-dd)? ")
|
||||||
edate = input("enter end date (yyyy-mm-dd)? ")
|
edate = input("enter end date (yyyy-mm-dd)? ")
|
||||||
|
|
||||||
f = open(inputFile)
|
f = Path.open(inputFile)
|
||||||
try:
|
try:
|
||||||
fileLines = f.readlines()
|
fileLines = f.readlines()
|
||||||
finally:
|
finally:
|
||||||
@@ -333,7 +329,7 @@ def buildDatabaseFromFile(inputFile, inputType, inputProduct, inputUser):
|
|||||||
database.globalUkedagForbruk = [0] * 7
|
database.globalUkedagForbruk = [0] * 7
|
||||||
database.pengebeholdning = [0] * (inputLine.numberOfDays + 1)
|
database.pengebeholdning = [0] * (inputLine.numberOfDays + 1)
|
||||||
for linje in fileLines:
|
for linje in fileLines:
|
||||||
if not (linje[0] == "#") and not (linje == "\n"):
|
if linje[0] != "#" and linje != "\n":
|
||||||
# henter dateNum, products, user, price
|
# henter dateNum, products, user, price
|
||||||
restDel = linje.partition("|")
|
restDel = linje.partition("|")
|
||||||
restDel = restDel[2].partition(" ")
|
restDel = restDel[2].partition(" ")
|
||||||
@@ -363,7 +359,7 @@ def buildDatabaseFromFile(inputFile, inputType, inputProduct, inputUser):
|
|||||||
return database, dateLine
|
return database, dateLine
|
||||||
|
|
||||||
|
|
||||||
def printTopDict(dictionary, n, k):
|
def printTopDict(dictionary: dict[str, Any], n: int, k: bool) -> None:
|
||||||
i = 0
|
i = 0
|
||||||
for key in sorted(dictionary, key=dictionary.get, reverse=k):
|
for key in sorted(dictionary, key=dictionary.get, reverse=k):
|
||||||
print(key, ": ", dictionary[key])
|
print(key, ": ", dictionary[key])
|
||||||
@@ -373,7 +369,7 @@ def printTopDict(dictionary, n, k):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def printTopDict2(dictionary, dictionary2, n):
|
def printTopDict2(dictionary, dictionary2, n) -> None:
|
||||||
print("")
|
print("")
|
||||||
print("product : price[kr] ( number )")
|
print("product : price[kr] ( number )")
|
||||||
i = 0
|
i = 0
|
||||||
@@ -385,7 +381,7 @@ def printTopDict2(dictionary, dictionary2, n):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def printWeekdays(week, days):
|
def printWeekdays(week, days) -> None:
|
||||||
if week == [] or days == 0:
|
if week == [] or days == 0:
|
||||||
return
|
return
|
||||||
print(
|
print(
|
||||||
@@ -408,10 +404,10 @@ def printWeekdays(week, days):
|
|||||||
print("")
|
print("")
|
||||||
|
|
||||||
|
|
||||||
def printBalance(database, user):
|
def printBalance(database, user) -> None:
|
||||||
forbruk = 0
|
forbruk = 0
|
||||||
if user in database.personVareVerdi:
|
if user in database.personVareVerdi:
|
||||||
forbruk = sum([i for i in list(database.personVareVerdi[user].values())])
|
forbruk = sum(database.personVareVerdi[user].values())
|
||||||
print("totalt kjøpt for: ", forbruk, end=" ")
|
print("totalt kjøpt for: ", forbruk, end=" ")
|
||||||
if user in database.personNegTransactions:
|
if user in database.personNegTransactions:
|
||||||
print("kr, totalt lagt til: ", -database.personNegTransactions[user], end=" ")
|
print("kr, totalt lagt til: ", -database.personNegTransactions[user], end=" ")
|
||||||
@@ -423,14 +419,14 @@ def printBalance(database, user):
|
|||||||
print("")
|
print("")
|
||||||
|
|
||||||
|
|
||||||
def printUser(database, dateLine, user, n):
|
def printUser(database, dateLine, user, n) -> None:
|
||||||
printTopDict2(database.personVareVerdi[user], database.personVareAntall[user], n)
|
printTopDict2(database.personVareVerdi[user], database.personVareAntall[user], n)
|
||||||
print("\nforbruk per ukedag [kr/dag],", end=" ")
|
print("\nforbruk per ukedag [kr/dag],", end=" ")
|
||||||
printWeekdays(database.personUkedagVerdi[user], len(dateLine))
|
printWeekdays(database.personUkedagVerdi[user], len(dateLine))
|
||||||
printBalance(database, user)
|
printBalance(database, user)
|
||||||
|
|
||||||
|
|
||||||
def printProduct(database, dateLine, product, n):
|
def printProduct(database, dateLine, product, n) -> None:
|
||||||
printTopDict(database.varePersonAntall[product], n, 1)
|
printTopDict(database.varePersonAntall[product], n, 1)
|
||||||
print("\nforbruk per ukedag [antall/dag],", end=" ")
|
print("\nforbruk per ukedag [antall/dag],", end=" ")
|
||||||
printWeekdays(database.vareUkedagAntall[product], len(dateLine))
|
printWeekdays(database.vareUkedagAntall[product], len(dateLine))
|
||||||
@@ -444,7 +440,7 @@ def printProduct(database, dateLine, product, n):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def printGlobal(database, dateLine, n):
|
def printGlobal(database, dateLine, n) -> None:
|
||||||
print("\nmest lagt til: ")
|
print("\nmest lagt til: ")
|
||||||
printTopDict(database.personNegTransactions, n, 0)
|
printTopDict(database.personNegTransactions, n, 0)
|
||||||
print("\nmest tatt fra:")
|
print("\nmest tatt fra:")
|
||||||
@@ -458,9 +454,9 @@ def printGlobal(database, dateLine, n):
|
|||||||
"Det er solgt varer til en verdi av: ",
|
"Det er solgt varer til en verdi av: ",
|
||||||
sum(database.globalDatoForbruk),
|
sum(database.globalDatoForbruk),
|
||||||
"kr, det er lagt til",
|
"kr, det er lagt til",
|
||||||
-sum([i for i in list(database.personNegTransactions.values())]),
|
-sum(database.personNegTransactions.values()),
|
||||||
"og tatt fra",
|
"og tatt fra",
|
||||||
sum([i for i in list(database.personPosTransactions.values())]),
|
sum(database.personPosTransactions.values()),
|
||||||
end=" ",
|
end=" ",
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
@@ -470,23 +466,24 @@ def printGlobal(database, dateLine, n):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def alt4menuTextOnly(database, dateLine):
|
def alt4menuTextOnly(database, dateLine, sql_session: Session) -> None:
|
||||||
|
assert sql_session is not None
|
||||||
n = 10
|
n = 10
|
||||||
while 1:
|
while 1:
|
||||||
print(
|
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("")
|
inp = input("")
|
||||||
if inp == "q":
|
if inp == "q":
|
||||||
break
|
break
|
||||||
elif inp == "1":
|
if inp == "1":
|
||||||
try:
|
try:
|
||||||
printUser(database, dateLine, getUser(), n)
|
printUser(database, dateLine, getUser(sql_session), n)
|
||||||
except:
|
except:
|
||||||
print("\n\nSomething is not right, (last date prior to first date?)")
|
print("\n\nSomething is not right, (last date prior to first date?)")
|
||||||
elif inp == "2":
|
elif inp == "2":
|
||||||
try:
|
try:
|
||||||
printProduct(database, dateLine, getProduct(), n)
|
printProduct(database, dateLine, getProduct(sql_session), n)
|
||||||
except:
|
except:
|
||||||
print("\n\nSomething is not right, (last date prior to first date?)")
|
print("\n\nSomething is not right, (last date prior to first date?)")
|
||||||
elif inp == "3":
|
elif inp == "3":
|
||||||
@@ -498,15 +495,16 @@ def alt4menuTextOnly(database, dateLine):
|
|||||||
n = int(input("set number to show "))
|
n = int(input("set number to show "))
|
||||||
|
|
||||||
|
|
||||||
def statisticsTextOnly():
|
def statisticsTextOnly(sql_session: Session) -> None:
|
||||||
|
assert sql_session is not None
|
||||||
inputType = 4
|
inputType = 4
|
||||||
product = ""
|
product = ""
|
||||||
user = ""
|
user = ""
|
||||||
print("\n0: from file, 1: from database, q:quit")
|
print("\n0: from file, 1: from database, q:quit")
|
||||||
inp = input("")
|
inp = input("")
|
||||||
if inp == "1":
|
if inp == "1":
|
||||||
database, dateLine = buildDatabaseFromDb(inputType, product, user)
|
database, dateLine = buildDatabaseFromDb(inputType, product, user, sql_session)
|
||||||
elif inp == "0" or inp == "":
|
elif inp == "0" or inp == "":
|
||||||
database, dateLine = buildDatabaseFromFile("default.dibblerlog", inputType, product, user)
|
database, dateLine = buildDatabaseFromFile("default.dibblerlog", inputType, product, user)
|
||||||
if not inp == "q":
|
if inp != "q":
|
||||||
alt4menuTextOnly(database, dateLine)
|
alt4menuTextOnly(database, dateLine, sql_session)
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from dibbler.conf import config
|
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
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
@@ -8,38 +14,78 @@ parser.add_argument(
|
|||||||
"-c",
|
"-c",
|
||||||
"--config",
|
"--config",
|
||||||
help="Path to the config file",
|
help="Path to the config file",
|
||||||
type=str,
|
type=Path,
|
||||||
|
metavar="FILE",
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-V",
|
||||||
|
"--version",
|
||||||
|
help="Show program version",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(
|
subparsers = parser.add_subparsers(
|
||||||
title="subcommands",
|
title="subcommands",
|
||||||
dest="subcommand",
|
dest="subcommand",
|
||||||
required=True,
|
|
||||||
)
|
)
|
||||||
subparsers.add_parser("loop", help="Run the dibbler loop")
|
subparsers.add_parser("loop", help="Run the dibbler loop")
|
||||||
subparsers.add_parser("create-db", help="Create the database")
|
subparsers.add_parser("create-db", help="Create the database")
|
||||||
subparsers.add_parser("slabbedasker", help="Find out who is slabbedasker")
|
subparsers.add_parser("slabbedasker", help="Find out who is slabbedasker")
|
||||||
|
subparsers.add_parser("seed-data", help="Fill with mock data")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
config.read(args.config)
|
|
||||||
|
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.subcommand == "loop":
|
if args.subcommand == "loop":
|
||||||
import dibbler.subcommands.loop as loop
|
import dibbler.subcommands.loop as loop
|
||||||
|
|
||||||
loop.main()
|
loop.main(sql_session)
|
||||||
|
|
||||||
elif args.subcommand == "create-db":
|
elif args.subcommand == "create-db":
|
||||||
import dibbler.subcommands.makedb as makedb
|
import dibbler.subcommands.makedb as makedb
|
||||||
|
|
||||||
makedb.main()
|
makedb.main(engine)
|
||||||
|
|
||||||
elif args.subcommand == "slabbedasker":
|
elif args.subcommand == "slabbedasker":
|
||||||
import dibbler.subcommands.slabbedasker as slabbedasker
|
import dibbler.subcommands.slabbedasker as slabbedasker
|
||||||
|
|
||||||
slabbedasker.main()
|
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)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -26,28 +26,28 @@ __all__ = [
|
|||||||
from .addstock import AddStockMenu
|
from .addstock import AddStockMenu
|
||||||
from .buymenu import BuyMenu
|
from .buymenu import BuyMenu
|
||||||
from .editing import (
|
from .editing import (
|
||||||
AddUserMenu,
|
|
||||||
EditUserMenu,
|
|
||||||
AddProductMenu,
|
AddProductMenu,
|
||||||
EditProductMenu,
|
AddUserMenu,
|
||||||
AdjustStockMenu,
|
AdjustStockMenu,
|
||||||
CleanupStockMenu,
|
CleanupStockMenu,
|
||||||
|
EditProductMenu,
|
||||||
|
EditUserMenu,
|
||||||
)
|
)
|
||||||
from .faq import FAQMenu
|
from .faq import FAQMenu
|
||||||
from .helpermenus import Menu
|
from .helpermenus import Menu
|
||||||
from .mainmenu import MainMenu
|
from .mainmenu import MainMenu
|
||||||
from .miscmenus import (
|
from .miscmenus import (
|
||||||
ProductSearchMenu,
|
|
||||||
TransferMenu,
|
|
||||||
AdjustCreditMenu,
|
AdjustCreditMenu,
|
||||||
UserListMenu,
|
|
||||||
ShowUserMenu,
|
|
||||||
ProductListMenu,
|
ProductListMenu,
|
||||||
|
ProductSearchMenu,
|
||||||
|
ShowUserMenu,
|
||||||
|
TransferMenu,
|
||||||
|
UserListMenu,
|
||||||
)
|
)
|
||||||
from .printermenu import PrintLabelMenu
|
from .printermenu import PrintLabelMenu
|
||||||
from .stats import (
|
from .stats import (
|
||||||
ProductPopularityMenu,
|
|
||||||
ProductRevenueMenu,
|
|
||||||
BalanceMenu,
|
BalanceMenu,
|
||||||
LoggedStatisticsMenu,
|
LoggedStatisticsMenu,
|
||||||
|
ProductPopularityMenu,
|
||||||
|
ProductRevenueMenu,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.models import (
|
from dibbler.models import (
|
||||||
Product,
|
Product,
|
||||||
@@ -9,12 +10,13 @@ from dibbler.models import (
|
|||||||
Transaction,
|
Transaction,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .helpermenus import Menu
|
from .helpermenus import Menu
|
||||||
|
|
||||||
|
|
||||||
class AddStockMenu(Menu):
|
class AddStockMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Add stock and adjust credit", uses_db=True)
|
super().__init__("Add stock and adjust credit", sql_session)
|
||||||
self.help_text = """
|
self.help_text = """
|
||||||
Enter what you have bought for PVVVV here, along with your user name and how
|
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"""
|
much money you're due in credits for the purchase when prompted.\n"""
|
||||||
@@ -23,7 +25,7 @@ much money you're due in credits for the purchase when prompted.\n"""
|
|||||||
self.products = {}
|
self.products = {}
|
||||||
self.price = 0
|
self.price = 0
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> bool | None:
|
||||||
questions = {
|
questions = {
|
||||||
(
|
(
|
||||||
False,
|
False,
|
||||||
@@ -86,10 +88,10 @@ much money you're due in credits for the purchase when prompted.\n"""
|
|||||||
|
|
||||||
self.perform_transaction()
|
self.perform_transaction()
|
||||||
|
|
||||||
def complete_input(self):
|
def complete_input(self) -> bool:
|
||||||
return bool(self.users) and len(self.products) and self.price
|
return self.users is not None and len(self.products) > 0 and self.price > 0
|
||||||
|
|
||||||
def print_info(self):
|
def print_info(self) -> None:
|
||||||
width = 6 + Product.name_length
|
width = 6 + Product.name_length
|
||||||
print()
|
print()
|
||||||
print(width * "-")
|
print(width * "-")
|
||||||
@@ -109,7 +111,12 @@ 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(f"{self.products[product][0]}".rjust(width - len(product.name)))
|
||||||
print(width * "-")
|
print(width * "-")
|
||||||
|
|
||||||
def add_thing_to_pending(self, thing, amount, price):
|
def add_thing_to_pending(
|
||||||
|
self,
|
||||||
|
thing: User | Product,
|
||||||
|
amount: int,
|
||||||
|
price: int,
|
||||||
|
) -> None:
|
||||||
if isinstance(thing, User):
|
if isinstance(thing, User):
|
||||||
self.users.append(thing)
|
self.users.append(thing)
|
||||||
elif thing in list(self.products.keys()):
|
elif thing in list(self.products.keys()):
|
||||||
@@ -119,7 +126,7 @@ much money you're due in credits for the purchase when prompted.\n"""
|
|||||||
else:
|
else:
|
||||||
self.products[thing] = [amount, price]
|
self.products[thing] = [amount, price]
|
||||||
|
|
||||||
def perform_transaction(self):
|
def perform_transaction(self) -> None:
|
||||||
print("Did you pay a different price?")
|
print("Did you pay a different price?")
|
||||||
if self.confirm(">", default=False):
|
if self.confirm(">", default=False):
|
||||||
self.price = self.input_int("How much did you pay?", 0, self.price, default=self.price)
|
self.price = self.input_int("How much did you pay?", 0, self.price, default=self.price)
|
||||||
@@ -132,10 +139,11 @@ much money you're due in credits for the purchase when prompted.\n"""
|
|||||||
old_price = product.price
|
old_price = product.price
|
||||||
old_hidden = product.hidden
|
old_hidden = product.hidden
|
||||||
product.price = int(
|
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(
|
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
|
product.hidden = False
|
||||||
print(
|
print(
|
||||||
@@ -151,13 +159,14 @@ much money you're due in credits for the purchase when prompted.\n"""
|
|||||||
PurchaseEntry(purchase, product, -self.products[product][0])
|
PurchaseEntry(purchase, product, -self.products[product][0])
|
||||||
|
|
||||||
purchase.perform_soft_purchase(-self.price, round_up=False)
|
purchase.perform_soft_purchase(-self.price, round_up=False)
|
||||||
self.session.add(purchase)
|
self.sql_session.add(purchase)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
print("Success! Transaction performed:")
|
print("Success! Transaction performed:")
|
||||||
# self.print_info()
|
# self.print_info()
|
||||||
for user in self.users:
|
for user in self.users:
|
||||||
print(f"User {user.name}'s credit is now {user.credit:d}")
|
print(f"User {user.name}'s credit is now {user.credit:d}")
|
||||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not perform transaction: {e}")
|
print(f"Could not perform transaction: {e}")
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.conf import config
|
from dibbler.conf import config
|
||||||
from dibbler.models import (
|
from dibbler.models import (
|
||||||
@@ -13,10 +17,11 @@ from .helpermenus import Menu
|
|||||||
|
|
||||||
|
|
||||||
class BuyMenu(Menu):
|
class BuyMenu(Menu):
|
||||||
def __init__(self, session=None):
|
superfast_mode: bool
|
||||||
Menu.__init__(self, "Buy", uses_db=True)
|
purchase: Purchase
|
||||||
if session:
|
|
||||||
self.session = session
|
def __init__(self, sql_session: Session) -> None:
|
||||||
|
super().__init__("Buy", sql_session)
|
||||||
self.superfast_mode = False
|
self.superfast_mode = False
|
||||||
self.help_text = """
|
self.help_text = """
|
||||||
Each purchase may contain one or more products and one or more buyers.
|
Each purchase may contain one or more products and one or more buyers.
|
||||||
@@ -28,7 +33,7 @@ addition, and you can type 'what' at any time to redisplay it.
|
|||||||
When finished, write an empty line to confirm the purchase.\n"""
|
When finished, write an empty line to confirm the purchase.\n"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def credit_check(user):
|
def credit_check(user: User) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param user:
|
:param user:
|
||||||
@@ -37,28 +42,32 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
"""
|
"""
|
||||||
assert isinstance(user, User)
|
assert isinstance(user, User)
|
||||||
|
|
||||||
return user.credit > config.getint("limits", "low_credit_warning_limit")
|
return user.credit > config["limits"]["low_credit_warning_limit"]
|
||||||
|
|
||||||
def low_credit_warning(self, user, timeout=False):
|
def low_credit_warning(
|
||||||
|
self,
|
||||||
|
user: User,
|
||||||
|
timeout: bool = False,
|
||||||
|
) -> bool:
|
||||||
assert isinstance(user, User)
|
assert isinstance(user, User)
|
||||||
|
|
||||||
print("***********************************************************************")
|
print(r"***********************************************************************")
|
||||||
print("***********************************************************************")
|
print(r"***********************************************************************")
|
||||||
print("")
|
print(r"")
|
||||||
print("$$\ $$\ $$$$$$\ $$$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$\\")
|
print(r"$$\ $$\ $$$$$$\ $$$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$\\")
|
||||||
print("$$ | $\ $$ |$$ __$$\ $$ __$$\ $$$\ $$ |\_$$ _|$$$\ $$ |$$ __$$\\")
|
print(r"$$ | $\ $$ |$$ __$$\ $$ __$$\ $$$\ $$ |\_$$ _|$$$\ $$ |$$ __$$\\")
|
||||||
print("$$ |$$$\ $$ |$$ / $$ |$$ | $$ |$$$$\ $$ | $$ | $$$$\ $$ |$$ / \__|")
|
print(r"$$ |$$$\ $$ |$$ / $$ |$$ | $$ |$$$$\ $$ | $$ | $$$$\ $$ |$$ / \__|")
|
||||||
print("$$ $$ $$\$$ |$$$$$$$$ |$$$$$$$ |$$ $$\$$ | $$ | $$ $$\$$ |$$ |$$$$\\")
|
print(r"$$ $$ $$\$$ |$$$$$$$$ |$$$$$$$ |$$ $$\$$ | $$ | $$ $$\$$ |$$ |$$$$\\")
|
||||||
print("$$$$ _$$$$ |$$ __$$ |$$ __$$< $$ \$$$$ | $$ | $$ \$$$$ |$$ |\_$$ |")
|
print(r"$$$$ _$$$$ |$$ __$$ |$$ __$$< $$ \$$$$ | $$ | $$ \$$$$ |$$ |\_$$ |")
|
||||||
print("$$$ / \$$$ |$$ | $$ |$$ | $$ |$$ |\$$$ | $$ | $$ |\$$$ |$$ | $$ |")
|
print(r"$$$ / \$$$ |$$ | $$ |$$ | $$ |$$ |\$$$ | $$ | $$ |\$$$ |$$ | $$ |")
|
||||||
print("$$ / \$$ |$$ | $$ |$$ | $$ |$$ | \$$ |$$$$$$\ $$ | \$$ |\$$$$$$ |")
|
print(r"$$ / \$$ |$$ | $$ |$$ | $$ |$$ | \$$ |$$$$$$\ $$ | \$$ |\$$$$$$ |")
|
||||||
print("\__/ \__|\__| \__|\__| \__|\__| \__|\______|\__| \__| \______/")
|
print(r"\__/ \__|\__| \__|\__| \__|\__| \__|\______|\__| \__| \______/")
|
||||||
print("")
|
print(r"")
|
||||||
print("***********************************************************************")
|
print(r"***********************************************************************")
|
||||||
print("***********************************************************************")
|
print(r"***********************************************************************")
|
||||||
print("")
|
print(r"")
|
||||||
print(
|
print(
|
||||||
f"USER {user.name} HAS LOWER CREDIT THAN {config.getint('limits', 'low_credit_warning_limit'):d}."
|
f"USER {user.name} HAS LOWER CREDIT THAN {config['limits']['low_credit_warning_limit']:d}.",
|
||||||
)
|
)
|
||||||
print("THIS PURCHASE WILL CHARGE YOUR CREDIT TWICE AS MUCH.")
|
print("THIS PURCHASE WILL CHARGE YOUR CREDIT TWICE AS MUCH.")
|
||||||
print("CONSIDER PUTTING MONEY IN THE BOX TO AVOID THIS.")
|
print("CONSIDER PUTTING MONEY IN THE BOX TO AVOID THIS.")
|
||||||
@@ -68,10 +77,13 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
if timeout:
|
if timeout:
|
||||||
print("THIS PURCHASE WILL AUTOMATICALLY BE PERFORMED IN 3 MINUTES!")
|
print("THIS PURCHASE WILL AUTOMATICALLY BE PERFORMED IN 3 MINUTES!")
|
||||||
return self.confirm(prompt=">", default=True, timeout=180)
|
return self.confirm(prompt=">", default=True, timeout=180)
|
||||||
else:
|
|
||||||
return self.confirm(prompt=">", default=True)
|
return self.confirm(prompt=">", default=True)
|
||||||
|
|
||||||
def add_thing_to_purchase(self, thing, amount=1):
|
def add_thing_to_purchase(
|
||||||
|
self,
|
||||||
|
thing: User | Product,
|
||||||
|
amount: int = 1,
|
||||||
|
) -> bool:
|
||||||
if isinstance(thing, User):
|
if isinstance(thing, User):
|
||||||
if thing.is_anonymous():
|
if thing.is_anonymous():
|
||||||
print("---------------------------------------------")
|
print("---------------------------------------------")
|
||||||
@@ -80,7 +92,10 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
print("---------------------------------------------")
|
print("---------------------------------------------")
|
||||||
|
|
||||||
if not self.credit_check(thing):
|
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)
|
Transaction(thing, purchase=self.purchase, penalty=2)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -95,7 +110,11 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
PurchaseEntry(self.purchase, thing, amount)
|
PurchaseEntry(self.purchase, thing, amount)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _execute(self, initial_contents=None):
|
def _execute(
|
||||||
|
self,
|
||||||
|
initial_contents: list[tuple[User | Product, int]] | None = None,
|
||||||
|
**_kwargs,
|
||||||
|
) -> bool:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
self.purchase = Purchase()
|
self.purchase = Purchase()
|
||||||
self.exit_confirm_msg = None
|
self.exit_confirm_msg = None
|
||||||
@@ -107,7 +126,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
for thing, num in initial_contents:
|
for thing, num in initial_contents:
|
||||||
self.add_thing_to_purchase(thing, num)
|
self.add_thing_to_purchase(thing, num)
|
||||||
|
|
||||||
def is_product(candidate):
|
def is_product(candidate: Any) -> bool:
|
||||||
return isinstance(candidate[0], Product)
|
return isinstance(candidate[0], Product)
|
||||||
|
|
||||||
if len(initial_contents) > 0 and all(map(is_product, initial_contents)):
|
if len(initial_contents) > 0 and all(map(is_product, initial_contents)):
|
||||||
@@ -129,7 +148,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
True,
|
True,
|
||||||
True,
|
True,
|
||||||
): "Enter more products or users, or an empty line to confirm",
|
): "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):
|
# Read in a 'thing' (product or user):
|
||||||
@@ -147,12 +166,12 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
if thing is None:
|
if thing is None:
|
||||||
if not self.complete_input():
|
if not self.complete_input():
|
||||||
if self.confirm(
|
if self.confirm(
|
||||||
"Not enough information entered. Abort purchase?", default=True
|
"Not enough information entered. Abort purchase?",
|
||||||
|
default=True,
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
else:
|
|
||||||
# once we get something in the
|
# once we get something in the
|
||||||
# purchase, we want to protect the
|
# purchase, we want to protect the
|
||||||
# user from accidentally killing it
|
# user from accidentally killing it
|
||||||
@@ -167,10 +186,11 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
break
|
break
|
||||||
|
|
||||||
self.purchase.perform_purchase()
|
self.purchase.perform_purchase()
|
||||||
self.session.add(self.purchase)
|
self.sql_session.add(self.purchase)
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not store purchase: {e}")
|
print(f"Could not store purchase: {e}")
|
||||||
else:
|
else:
|
||||||
print("Purchase stored.")
|
print("Purchase stored.")
|
||||||
@@ -178,9 +198,9 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
for t in self.purchase.transactions:
|
for t in self.purchase.transactions:
|
||||||
if not t.user.is_anonymous():
|
if not t.user.is_anonymous():
|
||||||
print(f"User {t.user.name}'s credit is now {t.user.credit:d} kr")
|
print(f"User {t.user.name}'s credit is now {t.user.credit:d} kr")
|
||||||
if t.user.credit < config.getint("limits", "low_credit_warning_limit"):
|
if t.user.credit < config["limits"]["low_credit_warning_limit"]:
|
||||||
print(
|
print(
|
||||||
f'USER {t.user.name} HAS LOWER CREDIT THAN {config.getint("limits", "low_credit_warning_limit"):d},',
|
f"USER {t.user.name} HAS LOWER CREDIT THAN {config['limits']['low_credit_warning_limit']:d},",
|
||||||
"AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.",
|
"AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -189,10 +209,10 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
print("")
|
print("")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def complete_input(self):
|
def complete_input(self) -> bool:
|
||||||
return self.purchase.is_complete()
|
return self.purchase.is_complete()
|
||||||
|
|
||||||
def format_purchase(self):
|
def format_purchase(self) -> str | None:
|
||||||
self.purchase.set_price()
|
self.purchase.set_price()
|
||||||
transactions = self.purchase.transactions
|
transactions = self.purchase.transactions
|
||||||
entries = self.purchase.entries
|
entries = self.purchase.entries
|
||||||
@@ -204,7 +224,10 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
string += "(empty)"
|
string += "(empty)"
|
||||||
else:
|
else:
|
||||||
string += ", ".join(
|
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: "
|
string += "\n products: "
|
||||||
if len(entries) == 0:
|
if len(entries) == 0:
|
||||||
@@ -212,7 +235,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
else:
|
else:
|
||||||
string += "\n "
|
string += "\n "
|
||||||
string += "\n ".join(
|
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:
|
if len(transactions) > 1:
|
||||||
string += f"\n price per person: {self.purchase.price_per_transaction():d} kr"
|
string += f"\n price per person: {self.purchase.price_per_transaction():d} kr"
|
||||||
@@ -228,7 +251,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def print_purchase(self):
|
def print_purchase(self) -> None:
|
||||||
info = self.format_purchase()
|
info = self.format_purchase()
|
||||||
if info is not None:
|
if info is not None:
|
||||||
self.set_context(info)
|
self.set_context(info)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import sqlalchemy
|
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
|
from .helpermenus import Menu, Selector
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -14,32 +17,48 @@ __all__ = [
|
|||||||
|
|
||||||
|
|
||||||
class AddUserMenu(Menu):
|
class AddUserMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Add user", uses_db=True)
|
super().__init__("Add user", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
username = self.input_str(
|
username = self.input_str(
|
||||||
"Username (should be same as PVV username)",
|
"Username (should be same as PVV username)",
|
||||||
regex=User.name_re,
|
regex=User.name_re,
|
||||||
length_range=(1, 10),
|
length_range=(1, 10),
|
||||||
)
|
)
|
||||||
cardnum = self.input_str("Card number (optional)", regex=User.card_re, length_range=(0, 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()
|
cardnum = cardnum.lower()
|
||||||
rfid = self.input_str("RFID (optional)", regex=User.rfid_re, length_range=(0, 10))
|
|
||||||
|
rfid = self.input_str(
|
||||||
|
"RFID (optional)",
|
||||||
|
regex=User.rfid_re,
|
||||||
|
length_range=(0, 10),
|
||||||
|
empty_string_is_none=True,
|
||||||
|
)
|
||||||
|
|
||||||
user = User(username, cardnum, rfid)
|
user = User(username, cardnum, rfid)
|
||||||
self.session.add(user)
|
self.sql_session.add(user)
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
print(f"User {username} stored")
|
print(f"User {username} stored")
|
||||||
except sqlalchemy.exc.IntegrityError as e:
|
except IntegrityError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not store user {username}: {e}")
|
print(f"Could not store user {username}: {e}")
|
||||||
self.pause()
|
self.pause()
|
||||||
|
|
||||||
|
|
||||||
class EditUserMenu(Menu):
|
class EditUserMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Edit user", uses_db=True)
|
super().__init__("Edit user", sql_session)
|
||||||
self.help_text = """
|
self.help_text = """
|
||||||
The only editable part of a user is its card number and rfid.
|
The only editable part of a user is its card number and rfid.
|
||||||
|
|
||||||
@@ -47,7 +66,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).
|
user, then rfid (write an empty line to remove the card number or rfid).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
user = self.input_user("User")
|
user = self.input_user("User")
|
||||||
self.printc(f"Editing user {user.name}")
|
self.printc(f"Editing user {user.name}")
|
||||||
@@ -69,43 +88,50 @@ user, then rfid (write an empty line to remove the card number or rfid).
|
|||||||
empty_string_is_none=True,
|
empty_string_is_none=True,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
print(f"User {user.name} stored")
|
print(f"User {user.name} stored")
|
||||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not store user {user.name}: {e}")
|
print(f"Could not store user {user.name}: {e}")
|
||||||
self.pause()
|
self.pause()
|
||||||
|
|
||||||
|
|
||||||
class AddProductMenu(Menu):
|
class AddProductMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Add product", uses_db=True)
|
super().__init__("Add product", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
bar_code = self.input_str("Bar code", regex=Product.bar_code_re, length_range=(8, 13))
|
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))
|
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)
|
price = self.input_int("Price", 1, 100000)
|
||||||
product = Product(bar_code, name, price)
|
product = Product(bar_code, name, price)
|
||||||
self.session.add(product)
|
self.sql_session.add(product)
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
print(f"Product {name} stored")
|
print(f"Product {name} stored")
|
||||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not store product {name}: {e}")
|
print(f"Could not store product {name}: {e}")
|
||||||
self.pause()
|
self.pause()
|
||||||
|
|
||||||
|
|
||||||
class EditProductMenu(Menu):
|
class EditProductMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Edit product", uses_db=True)
|
super().__init__("Edit product", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
product = self.input_product("Product")
|
product = self.input_product("Product")
|
||||||
self.printc(f"Editing product {product.name}")
|
self.printc(f"Editing product {product.name}")
|
||||||
while True:
|
while True:
|
||||||
selector = Selector(
|
selector = Selector(
|
||||||
f"Do what with {product.name}?",
|
f"Do what with {product.name}?",
|
||||||
|
sql_session=self.sql_session,
|
||||||
items=[
|
items=[
|
||||||
("name", "Edit name"),
|
("name", "Edit name"),
|
||||||
("price", "Edit price"),
|
("price", "Edit price"),
|
||||||
@@ -135,9 +161,10 @@ class EditProductMenu(Menu):
|
|||||||
product.hidden = self.confirm(f"Hidden(currently {product.hidden})", default=False)
|
product.hidden = self.confirm(f"Hidden(currently {product.hidden})", default=False)
|
||||||
elif what == "store":
|
elif what == "store":
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
print(f"Product {product.name} stored")
|
print(f"Product {product.name} stored")
|
||||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not store product {product.name}: {e}")
|
print(f"Could not store product {product.name}: {e}")
|
||||||
self.pause()
|
self.pause()
|
||||||
return
|
return
|
||||||
@@ -149,10 +176,10 @@ class EditProductMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class AdjustStockMenu(Menu):
|
class AdjustStockMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Adjust stock", uses_db=True)
|
super().__init__("Adjust stock", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
product = self.input_product("Product")
|
product = self.input_product("Product")
|
||||||
|
|
||||||
@@ -168,10 +195,11 @@ class AdjustStockMenu(Menu):
|
|||||||
product.stock += add_stock
|
product.stock += add_stock
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
print("Stock is now stored")
|
print("Stock is now stored")
|
||||||
self.pause()
|
self.pause()
|
||||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not store stock: {e}")
|
print(f"Could not store stock: {e}")
|
||||||
self.pause()
|
self.pause()
|
||||||
return
|
return
|
||||||
@@ -179,13 +207,13 @@ class AdjustStockMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class CleanupStockMenu(Menu):
|
class CleanupStockMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Stock Cleanup", uses_db=True)
|
super().__init__("Stock Cleanup", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
|
|
||||||
products = self.session.query(Product).filter(Product.stock != 0).all()
|
products = self.sql_session.query(Product).filter(Product.stock != 0).all()
|
||||||
|
|
||||||
print("Every product in stock will be printed.")
|
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.")
|
print("Entering no value will keep current stock or set it to 0 if it is negative.")
|
||||||
@@ -199,15 +227,16 @@ class CleanupStockMenu(Menu):
|
|||||||
for product in products:
|
for product in products:
|
||||||
oldstock = product.stock
|
oldstock = product.stock
|
||||||
product.stock = self.input_int(product.name, 0, 10000, default=max(0, oldstock))
|
product.stock = self.input_int(product.name, 0, 10000, default=max(0, oldstock))
|
||||||
self.session.add(product)
|
self.sql_session.add(product)
|
||||||
if oldstock != product.stock:
|
if oldstock != product.stock:
|
||||||
changed_products.append((product, oldstock))
|
changed_products.append((product, oldstock))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
print("New stocks are now stored.")
|
print("New stocks are now stored.")
|
||||||
self.pause()
|
self.pause()
|
||||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not store stock: {e}")
|
print(f"Could not store stock: {e}")
|
||||||
self.pause()
|
self.pause()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
from textwrap import dedent
|
||||||
|
|
||||||
from .helpermenus import MessageMenu, Menu
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from .helpermenus import Menu, MessageMenu
|
||||||
|
|
||||||
|
|
||||||
class FAQMenu(Menu):
|
class FAQMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Frequently Asked Questions")
|
super().__init__("Frequently Asked Questions", sql_session)
|
||||||
self.items = [
|
self.items = [
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"What is the meaning with this program?",
|
"What is the meaning with this program?",
|
||||||
"""
|
dedent("""
|
||||||
We want to avoid keeping lots of cash in PVVVV's money box and to
|
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
|
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
|
money each time, that is. You do of course have to pay for the things
|
||||||
@@ -21,18 +23,25 @@ class FAQMenu(Menu):
|
|||||||
stock and adjust credit".
|
stock and adjust credit".
|
||||||
Alternatively, add money to the money box and use "Adjust credit" to
|
Alternatively, add money to the money box and use "Adjust credit" to
|
||||||
tell Dibbler about it.
|
tell Dibbler about it.
|
||||||
""",
|
"""),
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Can I still pay for stuff using cash?",
|
"Can I still pay for stuff using cash?",
|
||||||
"""
|
dedent("""
|
||||||
Please put money in the money box and use "Adjust Credit" so that
|
Please put money in the money box and use "Adjust Credit" so that
|
||||||
dibbler can keep track of credit and purchases.""",
|
dibbler can keep track of credit and purchases.
|
||||||
|
"""),
|
||||||
|
sql_session,
|
||||||
|
),
|
||||||
|
MessageMenu(
|
||||||
|
"How do I exit from a submenu/dialog/thing?",
|
||||||
|
'Type "exit", "q", or ^d.',
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu("How do I exit from a submenu/dialog/thing?", 'Type "exit", "q", or ^d.'),
|
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
'What does "." mean?',
|
'What does "." mean?',
|
||||||
"""
|
dedent("""
|
||||||
The "." character, known as "full stop" or "period", is most often
|
The "." character, known as "full stop" or "period", is most often
|
||||||
used to indicate the end of a sentence.
|
used to indicate the end of a sentence.
|
||||||
|
|
||||||
@@ -40,41 +49,47 @@ class FAQMenu(Menu):
|
|||||||
read some text before continuing. Whenever some output ends with a
|
read some text before continuing. Whenever some output ends with a
|
||||||
line containing only a period, you should read the lines above and
|
line containing only a period, you should read the lines above and
|
||||||
then press enter to continue.
|
then press enter to continue.
|
||||||
""",
|
"""),
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Why is the user interface so terribly unintuitive?",
|
"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
|
Answer #2: We are trying to compete with PVV's microwave oven in
|
||||||
userfriendliness.
|
userfriendliness.
|
||||||
|
|
||||||
Answer #3: YOU are unintuitive.
|
Answer #3: YOU are unintuitive.
|
||||||
""",
|
"""),
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Why is there no help command?",
|
"Why is there no help command?",
|
||||||
'There is. Have you tried typing "help"?',
|
'There is. Have you tried typing "help"?',
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
'Where are the easter eggs? I tried saying "moo", but nothing happened.',
|
'Where are the easter eggs? I tried saying "moo", but nothing happened.',
|
||||||
'Don\'t say "moo".',
|
'Don\'t say "moo".',
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Why does the program speak English when all the users are Norwegians?",
|
"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.",
|
"Godt spørsmål. Det virket sikkert som en god idé der og da.",
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"Why does the screen have strange colours?",
|
"Why does the screen have strange colours?",
|
||||||
"""
|
dedent("""
|
||||||
Type "c" on the main menu to change the colours of the display, or
|
Type "c" on the main menu to change the colours of the display, or
|
||||||
"cs" if you are a boring person.
|
"cs" if you are a boring person.
|
||||||
""",
|
"""),
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"I found a bug; is there a reward?",
|
"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
|
But if you are certain that it is a bug, not a feature, then you
|
||||||
@@ -100,11 +115,12 @@ class FAQMenu(Menu):
|
|||||||
|
|
||||||
6. Type "restart" in Dibbler to replace the running process by a new
|
6. Type "restart" in Dibbler to replace the running process by a new
|
||||||
one using the updated files.
|
one using the updated files.
|
||||||
""",
|
"""),
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
MessageMenu(
|
MessageMenu(
|
||||||
"My question isn't listed here; what do I do?",
|
"My question isn't listed here; what do I do?",
|
||||||
"""
|
dedent("""
|
||||||
DON'T PANIC.
|
DON'T PANIC.
|
||||||
|
|
||||||
Follow this procedure:
|
Follow this procedure:
|
||||||
@@ -124,6 +140,7 @@ class FAQMenu(Menu):
|
|||||||
|
|
||||||
5. Type "restart" in Dibbler to replace the running process by a new
|
5. Type "restart" in Dibbler to replace the running process by a new
|
||||||
one using the updated files.
|
one using the updated files.
|
||||||
""",
|
"""),
|
||||||
|
sql_session,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,44 +1,64 @@
|
|||||||
# -*- coding: utf-8 -*-
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from select import select
|
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 (
|
from dibbler.lib.helpers import (
|
||||||
search_user,
|
|
||||||
search_product,
|
|
||||||
guess_data_type,
|
|
||||||
argmax,
|
argmax,
|
||||||
|
guess_data_type,
|
||||||
|
search_product,
|
||||||
|
search_user,
|
||||||
)
|
)
|
||||||
|
from dibbler.models import Product, User
|
||||||
|
|
||||||
exit_commands = ["exit", "abort", "quit", "bye", "eat flaming death", "q"]
|
if TYPE_CHECKING:
|
||||||
help_commands = ["help", "?"]
|
from collections.abc import Callable, Iterable
|
||||||
context_commands = ["what", "??"]
|
|
||||||
local_help_commands = ["help!", "???"]
|
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!", "???"]
|
||||||
|
|
||||||
|
|
||||||
class ExitMenu(Exception):
|
class ExitMenuException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Menu(object):
|
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
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name,
|
name: str,
|
||||||
items=None,
|
sql_session: Session,
|
||||||
prompt=None,
|
items: list[Self | tuple[MenuItemType, str] | str] | None = None,
|
||||||
end_prompt="> ",
|
prompt: str | None = None,
|
||||||
return_index=True,
|
end_prompt: str | None = "> ",
|
||||||
exit_msg=None,
|
return_index: bool = True,
|
||||||
exit_confirm_msg=None,
|
exit_msg: str | None = None,
|
||||||
exit_disallowed_msg=None,
|
exit_confirm_msg: str | None = None,
|
||||||
help_text=None,
|
exit_disallowed_msg: str | None = None,
|
||||||
uses_db=False,
|
help_text: str | None = None,
|
||||||
):
|
) -> None:
|
||||||
self.name = name
|
self.name: str = name
|
||||||
|
self.sql_session: Session = sql_session
|
||||||
self.items = items if items is not None else []
|
self.items = items if items is not None else []
|
||||||
self.prompt = prompt
|
self.prompt = prompt
|
||||||
self.end_prompt = end_prompt
|
self.end_prompt = end_prompt
|
||||||
@@ -48,54 +68,61 @@ class Menu(object):
|
|||||||
self.exit_disallowed_msg = exit_disallowed_msg
|
self.exit_disallowed_msg = exit_disallowed_msg
|
||||||
self.help_text = help_text
|
self.help_text = help_text
|
||||||
self.context = None
|
self.context = None
|
||||||
self.uses_db = uses_db
|
|
||||||
self.session = None
|
|
||||||
|
|
||||||
def exit_menu(self):
|
assert name is not None
|
||||||
|
assert self.sql_session is not None
|
||||||
|
|
||||||
|
def exit_menu(self) -> None:
|
||||||
if self.exit_disallowed_msg is not None:
|
if self.exit_disallowed_msg is not None:
|
||||||
print(self.exit_disallowed_msg)
|
print(self.exit_disallowed_msg)
|
||||||
return
|
return
|
||||||
if self.exit_confirm_msg is not None:
|
if self.exit_confirm_msg is not None:
|
||||||
if not self.confirm(self.exit_confirm_msg, default=True):
|
if not self.confirm(self.exit_confirm_msg, default=True):
|
||||||
return
|
return
|
||||||
raise ExitMenu()
|
raise ExitMenuException()
|
||||||
|
|
||||||
def at_exit(self):
|
def at_exit(self) -> None:
|
||||||
if self.exit_msg:
|
if self.exit_msg:
|
||||||
print(self.exit_msg)
|
print(self.exit_msg)
|
||||||
|
|
||||||
def set_context(self, string, display=True):
|
def set_context(
|
||||||
|
self,
|
||||||
|
string: str | None,
|
||||||
|
display: bool = True,
|
||||||
|
) -> None:
|
||||||
self.context = string
|
self.context = string
|
||||||
if self.context is not None and display:
|
if self.context is not None and display:
|
||||||
print(self.context)
|
print(self.context)
|
||||||
|
|
||||||
def add_to_context(self, string):
|
def add_to_context(self, string: str) -> None:
|
||||||
|
if self.context is not None:
|
||||||
self.context += string
|
self.context += string
|
||||||
|
else:
|
||||||
|
self.context = string
|
||||||
|
|
||||||
def printc(self, string):
|
def printc(self, string: str) -> None:
|
||||||
print(string)
|
print(string)
|
||||||
if self.context is None:
|
if self.context is None:
|
||||||
self.context = string
|
self.context = string
|
||||||
else:
|
else:
|
||||||
self.context += "\n" + string
|
self.context += "\n" + string
|
||||||
|
|
||||||
def show_context(self):
|
def show_context(self) -> None:
|
||||||
print(self.header())
|
print(self.header())
|
||||||
if self.context is not None:
|
if self.context is not None:
|
||||||
print(self.context)
|
print(self.context)
|
||||||
|
|
||||||
def item_is_submenu(self, i):
|
def item_is_submenu(self, i: int) -> bool:
|
||||||
return isinstance(self.items[i], Menu)
|
return isinstance(self.items[i], Menu)
|
||||||
|
|
||||||
def item_name(self, i):
|
def item_name(self, i: int) -> str:
|
||||||
if self.item_is_submenu(i):
|
if self.item_is_submenu(i):
|
||||||
return self.items[i].name
|
return self.items[i].name
|
||||||
elif isinstance(self.items[i], tuple):
|
if isinstance(self.items[i], tuple):
|
||||||
return self.items[i][1]
|
return self.items[i][1]
|
||||||
else:
|
|
||||||
return self.items[i]
|
return self.items[i]
|
||||||
|
|
||||||
def item_value(self, i):
|
def item_value(self, i: int) -> MenuItemType | int:
|
||||||
if isinstance(self.items[i], tuple):
|
if isinstance(self.items[i], tuple):
|
||||||
return self.items[i][0]
|
return self.items[i][0]
|
||||||
if self.return_index:
|
if self.return_index:
|
||||||
@@ -104,14 +131,14 @@ class Menu(object):
|
|||||||
|
|
||||||
def input_str(
|
def input_str(
|
||||||
self,
|
self,
|
||||||
prompt=None,
|
prompt: str | None = None,
|
||||||
end_prompt=None,
|
end_prompt: str | None = None,
|
||||||
regex=None,
|
regex: str | None = None,
|
||||||
length_range=(None, None),
|
length_range: tuple[int | None, int | None] = (None, None),
|
||||||
empty_string_is_none=False,
|
empty_string_is_none: bool = False,
|
||||||
timeout=None,
|
timeout: int | None = None,
|
||||||
default=None,
|
default: str | None = None,
|
||||||
):
|
) -> str | None:
|
||||||
if prompt is None:
|
if prompt is None:
|
||||||
prompt = self.prompt if self.prompt is not None else ""
|
prompt = self.prompt if self.prompt is not None else ""
|
||||||
if default is not None:
|
if default is not None:
|
||||||
@@ -168,7 +195,7 @@ class Menu(object):
|
|||||||
):
|
):
|
||||||
if length_range[0] and length_range[1]:
|
if length_range[0] and length_range[1]:
|
||||||
print(
|
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]:
|
elif length_range[0]:
|
||||||
print(f"Value must have length at least {length_range[0]:d}")
|
print(f"Value must have length at least {length_range[0]:d}")
|
||||||
@@ -177,7 +204,7 @@ class Menu(object):
|
|||||||
continue
|
continue
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def special_input_options(self, result):
|
def special_input_options(self, result) -> bool:
|
||||||
"""
|
"""
|
||||||
Handles special, magic input for input_str
|
Handles special, magic input for input_str
|
||||||
|
|
||||||
@@ -187,7 +214,7 @@ class Menu(object):
|
|||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def special_input_choice(self, in_str):
|
def special_input_choice(self, in_str: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Handle choices which are not simply menu items.
|
Handle choices which are not simply menu items.
|
||||||
|
|
||||||
@@ -197,33 +224,39 @@ class Menu(object):
|
|||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def input_choice(self, number_of_choices, prompt=None, end_prompt=None):
|
def input_choice(
|
||||||
|
self,
|
||||||
|
number_of_choices: int,
|
||||||
|
prompt: str | None = None,
|
||||||
|
end_prompt: str | None = None,
|
||||||
|
) -> int:
|
||||||
while True:
|
while True:
|
||||||
result = self.input_str(prompt, end_prompt)
|
result = self.input_str(prompt, end_prompt)
|
||||||
|
assert result is not None
|
||||||
if result == "":
|
if result == "":
|
||||||
print("Please enter something")
|
print("Please enter something")
|
||||||
else:
|
else:
|
||||||
if result.isdigit():
|
if result.isdigit():
|
||||||
choice = int(result)
|
choice = int(result)
|
||||||
if choice == 0 and 10 <= number_of_choices:
|
if choice == 0 and number_of_choices >= 10:
|
||||||
return 10
|
return 10
|
||||||
if 0 < choice <= number_of_choices:
|
if 0 < choice <= number_of_choices:
|
||||||
return choice
|
return choice
|
||||||
if not self.special_input_choice(result):
|
if not self.special_input_choice(result):
|
||||||
self.invalid_menu_choice(result)
|
self.invalid_menu_choice(result)
|
||||||
|
|
||||||
def invalid_menu_choice(self, in_str):
|
def invalid_menu_choice(self, in_str: str) -> None:
|
||||||
print("Please enter a valid choice.")
|
print("Please enter a valid choice.")
|
||||||
|
|
||||||
def input_int(
|
def input_int(
|
||||||
self,
|
self,
|
||||||
prompt=None,
|
prompt: str,
|
||||||
minimum=None,
|
minimum: int | None = None,
|
||||||
maximum=None,
|
maximum: int | None = None,
|
||||||
null_allowed=False,
|
null_allowed: bool = False,
|
||||||
zero_allowed=True,
|
zero_allowed: bool = True,
|
||||||
default=None,
|
default: int | None = None,
|
||||||
):
|
) -> int | Literal[False]:
|
||||||
if minimum is not None and maximum is not None:
|
if minimum is not None and maximum is not None:
|
||||||
end_prompt = f"({minimum}-{maximum})>"
|
end_prompt = f"({minimum}-{maximum})>"
|
||||||
elif minimum is not None:
|
elif minimum is not None:
|
||||||
@@ -234,7 +267,11 @@ class Menu(object):
|
|||||||
end_prompt = ""
|
end_prompt = ""
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
result = self.input_str(prompt + end_prompt, default=default)
|
result = self.input_str(
|
||||||
|
prompt + end_prompt,
|
||||||
|
default=str(default) if default is not None else None,
|
||||||
|
)
|
||||||
|
assert result is not None
|
||||||
if result == "" and null_allowed:
|
if result == "" and null_allowed:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
@@ -252,62 +289,81 @@ class Menu(object):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
print("Please enter an integer")
|
print("Please enter an integer")
|
||||||
|
|
||||||
def input_user(self, prompt=None, end_prompt=None):
|
def input_user(
|
||||||
|
self,
|
||||||
|
prompt: str | None = None,
|
||||||
|
end_prompt: str | None = None,
|
||||||
|
) -> User:
|
||||||
user = None
|
user = None
|
||||||
while user is None:
|
while user is None:
|
||||||
user = self.retrieve_user(self.input_str(prompt, end_prompt))
|
search_string = self.input_str(prompt, end_prompt)
|
||||||
|
assert search_string is not None
|
||||||
|
user = self.retrieve_user(search_string)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def retrieve_user(self, search_str):
|
def retrieve_user(self, search_str: str) -> User | None:
|
||||||
return self.search_ui(search_user, search_str, "user")
|
return self.search_ui(search_user, search_str, "user")
|
||||||
|
|
||||||
def input_product(self, prompt=None, end_prompt=None):
|
def input_product(
|
||||||
|
self,
|
||||||
|
prompt: str | None = None,
|
||||||
|
end_prompt: str | None = None,
|
||||||
|
) -> Product:
|
||||||
product = None
|
product = None
|
||||||
while product is None:
|
while product is None:
|
||||||
product = self.retrieve_product(self.input_str(prompt, end_prompt))
|
search_string = self.input_str(prompt, end_prompt)
|
||||||
|
assert search_string is not None
|
||||||
|
product = self.retrieve_product(search_string)
|
||||||
return product
|
return product
|
||||||
|
|
||||||
def retrieve_product(self, search_str):
|
def retrieve_product(self, search_str: str) -> Product | None:
|
||||||
return self.search_ui(search_product, search_str, "product")
|
return self.search_ui(search_product, search_str, "product")
|
||||||
|
|
||||||
def input_thing(
|
def input_thing(
|
||||||
self,
|
self,
|
||||||
prompt=None,
|
prompt: str | None = None,
|
||||||
end_prompt=None,
|
end_prompt: str | None = None,
|
||||||
permitted_things=("user", "product"),
|
permitted_things: Iterable[str] = ("user", "product"),
|
||||||
add_nonexisting=(),
|
add_nonexisting: Iterable[str] = (),
|
||||||
empty_input_permitted=False,
|
empty_input_permitted: bool = False,
|
||||||
find_hidden_products=True,
|
find_hidden_products: bool = True,
|
||||||
):
|
) -> User | Product | None:
|
||||||
result = None
|
result = None
|
||||||
while result is None:
|
while result is None:
|
||||||
search_str = self.input_str(prompt, end_prompt)
|
search_str = self.input_str(prompt, end_prompt)
|
||||||
|
assert search_str is not None
|
||||||
if search_str == "" and empty_input_permitted:
|
if search_str == "" and empty_input_permitted:
|
||||||
return None
|
return None
|
||||||
result = self.search_for_thing(
|
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
|
return result
|
||||||
|
|
||||||
def input_multiple(
|
def input_multiple(
|
||||||
self,
|
self,
|
||||||
prompt=None,
|
prompt: str | None = None,
|
||||||
end_prompt=None,
|
end_prompt: str | None = None,
|
||||||
permitted_things=("user", "product"),
|
permitted_things: Iterable[str] = ("user", "product"),
|
||||||
add_nonexisting=(),
|
add_nonexisting: Iterable[str] = (),
|
||||||
empty_input_permitted=False,
|
empty_input_permitted: bool = False,
|
||||||
find_hidden_products=True,
|
find_hidden_products: bool = True,
|
||||||
):
|
) -> tuple[User | Product, int] | None:
|
||||||
result = None
|
result = None
|
||||||
num = 0
|
num = 0
|
||||||
while result is None:
|
while result is None:
|
||||||
search_str = self.input_str(prompt, end_prompt)
|
search_str = self.input_str(prompt, end_prompt)
|
||||||
|
assert search_str is not None
|
||||||
search_lst = search_str.split(" ")
|
search_lst = search_str.split(" ")
|
||||||
if search_str == "" and empty_input_permitted:
|
if search_str == "" and empty_input_permitted:
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
result = self.search_for_thing(
|
result = self.search_for_thing(
|
||||||
search_str, permitted_things, add_nonexisting, find_hidden_products
|
search_str,
|
||||||
|
permitted_things,
|
||||||
|
add_nonexisting,
|
||||||
|
find_hidden_products,
|
||||||
)
|
)
|
||||||
num = 1
|
num = 1
|
||||||
|
|
||||||
@@ -329,16 +385,19 @@ class Menu(object):
|
|||||||
|
|
||||||
def search_for_thing(
|
def search_for_thing(
|
||||||
self,
|
self,
|
||||||
search_str,
|
search_str: str,
|
||||||
permitted_things=("user", "product"),
|
permitted_things: Iterable[str] = ("user", "product"),
|
||||||
add_non_existing=(),
|
add_non_existing: Iterable[str] = (),
|
||||||
find_hidden_products=True,
|
find_hidden_products: bool = True,
|
||||||
):
|
) -> User | Product | None:
|
||||||
search_fun = {"user": search_user, "product": search_product}
|
search_fun = {
|
||||||
|
"user": search_user,
|
||||||
|
"product": search_product,
|
||||||
|
}
|
||||||
results = {}
|
results = {}
|
||||||
result_values = {}
|
result_values = {}
|
||||||
for thing in permitted_things:
|
for thing in permitted_things:
|
||||||
results[thing] = search_fun[thing](search_str, self.session, find_hidden_products)
|
results[thing] = search_fun[thing](search_str, self.sql_session, find_hidden_products)
|
||||||
result_values[thing] = self.search_result_value(results[thing])
|
result_values[thing] = self.search_result_value(results[thing])
|
||||||
selected_thing = argmax(result_values)
|
selected_thing = argmax(result_values)
|
||||||
if not results[selected_thing]:
|
if not results[selected_thing]:
|
||||||
@@ -353,10 +412,14 @@ class Menu(object):
|
|||||||
return self.search_add(search_str)
|
return self.search_add(search_str)
|
||||||
# print('No match found for "%s".' % search_str)
|
# print('No match found for "%s".' % search_str)
|
||||||
return None
|
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
|
@staticmethod
|
||||||
def search_result_value(result):
|
def search_result_value(result) -> Literal[0, 1, 2, 3]:
|
||||||
if result is None:
|
if result is None:
|
||||||
return 0
|
return 0
|
||||||
if not isinstance(result, list):
|
if not isinstance(result, list):
|
||||||
@@ -367,18 +430,19 @@ class Menu(object):
|
|||||||
return 2
|
return 2
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def search_add(self, string):
|
def search_add(self, string: str) -> User | None:
|
||||||
type_guess = guess_data_type(string)
|
type_guess = guess_data_type(string)
|
||||||
if type_guess == "username":
|
if type_guess == "username":
|
||||||
print(f'"{string}" looks like a username, but no such user exists.')
|
print(f'"{string}" looks like a username, but no such user exists.')
|
||||||
if self.confirm(f"Create user {string}?"):
|
if self.confirm(f"Create user {string}?"):
|
||||||
user = User(string, None)
|
user = User(string, None)
|
||||||
self.session.add(user)
|
self.sql_session.add(user)
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
if type_guess == "card":
|
if type_guess == "card":
|
||||||
selector = Selector(
|
selector = Selector(
|
||||||
f'"{string}" looks like a card number, but no user with that card number exists.',
|
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}"),
|
("create", f"Create user with card number {string}"),
|
||||||
("set", f"Set card number of an existing user to {string}"),
|
("set", f"Set card number of an existing user to {string}"),
|
||||||
@@ -387,12 +451,14 @@ class Menu(object):
|
|||||||
selection = selector.execute()
|
selection = selector.execute()
|
||||||
if selection == "create":
|
if selection == "create":
|
||||||
username = self.input_str(
|
username = self.input_str(
|
||||||
"Username for new user (should be same as PVV username)",
|
prompt="Username for new user (should be same as PVV username)",
|
||||||
User.name_re,
|
end_prompt=None,
|
||||||
(1, 10),
|
regex=User.name_re,
|
||||||
|
length_range=(1, 10),
|
||||||
)
|
)
|
||||||
|
assert username is not None
|
||||||
user = User(username, string)
|
user = User(username, string)
|
||||||
self.session.add(user)
|
self.sql_session.add(user)
|
||||||
return user
|
return user
|
||||||
if selection == "set":
|
if selection == "set":
|
||||||
user = self.input_user("User to set card number for")
|
user = self.input_user("User to set card number for")
|
||||||
@@ -405,11 +471,21 @@ class Menu(object):
|
|||||||
print(f'"{string}" looks like the bar code for a product, but no such product exists.')
|
print(f'"{string}" looks like the bar code for a product, but no such product exists.')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def search_ui(self, search_fun, search_str, thing):
|
def search_ui(
|
||||||
result = search_fun(search_str, self.session)
|
self,
|
||||||
|
search_fun: Callable[[str, Session], list[Any] | Any],
|
||||||
|
search_str: str,
|
||||||
|
thing: str,
|
||||||
|
) -> Any:
|
||||||
|
result = search_fun(search_str, self.sql_session)
|
||||||
return self.search_ui2(search_str, result, thing)
|
return self.search_ui2(search_str, result, thing)
|
||||||
|
|
||||||
def search_ui2(self, search_str, result, thing):
|
def search_ui2(
|
||||||
|
self,
|
||||||
|
search_str: str,
|
||||||
|
result: list[Any] | Any,
|
||||||
|
thing: str,
|
||||||
|
) -> Any:
|
||||||
if not isinstance(result, list):
|
if not isinstance(result, list):
|
||||||
return result
|
return result
|
||||||
if len(result) == 0:
|
if len(result) == 0:
|
||||||
@@ -429,25 +505,41 @@ class Menu(object):
|
|||||||
else:
|
else:
|
||||||
select_header = f'{len(result):d} {thing}s matching "{search_str}"'
|
select_header = f'{len(result):d} {thing}s matching "{search_str}"'
|
||||||
select_items = result
|
select_items = result
|
||||||
selector = Selector(select_header, items=select_items, return_index=False)
|
selector = Selector(
|
||||||
|
select_header,
|
||||||
|
self.sql_session,
|
||||||
|
items=select_items,
|
||||||
|
return_index=False,
|
||||||
|
)
|
||||||
return selector.execute()
|
return selector.execute()
|
||||||
|
|
||||||
@staticmethod
|
def confirm(
|
||||||
def confirm(prompt, end_prompt=None, default=None, timeout=None):
|
self,
|
||||||
return ConfirmMenu(prompt, end_prompt=None, default=default, timeout=timeout).execute()
|
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()
|
||||||
|
|
||||||
def header(self):
|
def header(self) -> str:
|
||||||
return f"[{self.name}]"
|
return f"[{self.name}]"
|
||||||
|
|
||||||
def print_header(self):
|
def print_header(self) -> None:
|
||||||
print("")
|
print("")
|
||||||
print(self.header())
|
print(self.header())
|
||||||
|
|
||||||
def pause(self):
|
def pause(self) -> None:
|
||||||
self.input_str(".", end_prompt="")
|
self.input_str(".", end_prompt="")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def general_help():
|
def general_help() -> None:
|
||||||
print(
|
print(
|
||||||
"""
|
"""
|
||||||
DIBBLER HELP
|
DIBBLER HELP
|
||||||
@@ -470,10 +562,10 @@ class Menu(object):
|
|||||||
of money PVVVV owes the user. This value decreases with the
|
of money PVVVV owes the user. This value decreases with the
|
||||||
appropriate amount when you register a purchase, and you may increase
|
appropriate amount when you register a purchase, and you may increase
|
||||||
it by putting money in the box and using the "Adjust credit" menu.
|
it by putting money in the box and using the "Adjust credit" menu.
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def local_help(self):
|
def local_help(self) -> None:
|
||||||
if self.help_text is None:
|
if self.help_text is None:
|
||||||
print("no help here")
|
print("no help here")
|
||||||
else:
|
else:
|
||||||
@@ -481,21 +573,15 @@ class Menu(object):
|
|||||||
print(f"Help for {self.header()}:")
|
print(f"Help for {self.header()}:")
|
||||||
print(self.help_text)
|
print(self.help_text)
|
||||||
|
|
||||||
def execute(self, **kwargs):
|
def execute(self, **_kwargs) -> MenuItemType | int | None:
|
||||||
self.set_context(None)
|
self.set_context(None)
|
||||||
try:
|
try:
|
||||||
if self.uses_db and not self.session:
|
return self._execute(**_kwargs)
|
||||||
self.session = Session()
|
except ExitMenuException:
|
||||||
return self._execute(**kwargs)
|
|
||||||
except ExitMenu:
|
|
||||||
self.at_exit()
|
self.at_exit()
|
||||||
return None
|
return None
|
||||||
finally:
|
|
||||||
if self.session is not None:
|
|
||||||
self.session.close()
|
|
||||||
self.session = None
|
|
||||||
|
|
||||||
def _execute(self, **kwargs):
|
def _execute(self, **_kwargs) -> MenuItemType | int | None:
|
||||||
while True:
|
while True:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
self.set_context(None)
|
self.set_context(None)
|
||||||
@@ -514,12 +600,21 @@ class Menu(object):
|
|||||||
|
|
||||||
|
|
||||||
class MessageMenu(Menu):
|
class MessageMenu(Menu):
|
||||||
def __init__(self, name, message, pause_after_message=True):
|
message: str
|
||||||
Menu.__init__(self, name)
|
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)
|
||||||
self.message = message.strip()
|
self.message = message.strip()
|
||||||
self.pause_after_message = pause_after_message
|
self.pause_after_message = pause_after_message
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
print("")
|
print("")
|
||||||
print(self.message)
|
print(self.message)
|
||||||
@@ -528,10 +623,17 @@ class MessageMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class ConfirmMenu(Menu):
|
class ConfirmMenu(Menu):
|
||||||
def __init__(self, prompt="confirm? ", end_prompt=": ", default=None, timeout=0):
|
def __init__(
|
||||||
Menu.__init__(
|
|
||||||
self,
|
self,
|
||||||
|
sql_session: Session,
|
||||||
|
prompt: str = "confirm? ",
|
||||||
|
end_prompt: str | None = ": ",
|
||||||
|
default: bool | None = None,
|
||||||
|
timeout: int | None = 0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
"question",
|
"question",
|
||||||
|
sql_session,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
end_prompt=end_prompt,
|
end_prompt=end_prompt,
|
||||||
exit_disallowed_msg="Please answer yes or no",
|
exit_disallowed_msg="Please answer yes or no",
|
||||||
@@ -539,45 +641,55 @@ class ConfirmMenu(Menu):
|
|||||||
self.default = default
|
self.default = default
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> bool:
|
||||||
options = {True: "[y]/n", False: "y/[n]", None: "y/n"}[self.default]
|
options = {True: "[Y/n]", False: "[y/N]", None: "[y/n]"}[self.default]
|
||||||
while True:
|
while True:
|
||||||
result = self.input_str(
|
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()
|
result = result.lower().strip()
|
||||||
if result in ["y", "yes"]:
|
if result in ["y", "yes"]:
|
||||||
return True
|
return True
|
||||||
elif result in ["n", "no"]:
|
if result in ["n", "no"]:
|
||||||
return False
|
return False
|
||||||
elif self.default is not None and result == "":
|
if self.default is not None and result == "":
|
||||||
return self.default
|
return self.default
|
||||||
else:
|
|
||||||
print("Please answer yes or no")
|
print("Please answer yes or no")
|
||||||
|
|
||||||
|
|
||||||
class Selector(Menu):
|
class Selector(Menu):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name,
|
name: str,
|
||||||
items=None,
|
sql_session: Session,
|
||||||
prompt="select",
|
items: list[Self | tuple[MenuItemType, str] | str] | None = None,
|
||||||
return_index=True,
|
prompt: str | None = "select",
|
||||||
exit_msg=None,
|
return_index: bool = True,
|
||||||
exit_confirm_msg=None,
|
exit_msg: str | None = None,
|
||||||
help_text=None,
|
exit_confirm_msg: str | None = None,
|
||||||
):
|
help_text: str | None = None,
|
||||||
|
) -> None:
|
||||||
if items is None:
|
if items is None:
|
||||||
items = []
|
items = []
|
||||||
Menu.__init__(self, name, items, prompt, return_index=return_index, exit_msg=exit_msg)
|
super().__init__(
|
||||||
|
name,
|
||||||
|
sql_session,
|
||||||
|
items,
|
||||||
|
prompt,
|
||||||
|
return_index=return_index,
|
||||||
|
exit_msg=exit_msg,
|
||||||
|
help_text=help_text,
|
||||||
|
)
|
||||||
|
|
||||||
def header(self):
|
def header(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def print_header(self):
|
def print_header(self) -> None:
|
||||||
print(self.header())
|
print(self.header())
|
||||||
|
|
||||||
def local_help(self):
|
def local_help(self) -> None:
|
||||||
if self.help_text is None:
|
if self.help_text is None:
|
||||||
print("This is a selection menu. Enter one of the listed numbers, or")
|
print("This is a selection menu. Enter one of the listed numbers, or")
|
||||||
print("'exit' to go out and do something else.")
|
print("'exit' to go out and do something else.")
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from dibbler.db import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .buymenu import BuyMenu
|
from .buymenu import BuyMenu
|
||||||
from .faq import FAQMenu
|
from .faq import FAQMenu
|
||||||
@@ -13,14 +12,17 @@ faq_commands = ["faq"]
|
|||||||
restart_commands = ["restart"]
|
restart_commands = ["restart"]
|
||||||
|
|
||||||
|
|
||||||
def restart():
|
def restart() -> None:
|
||||||
# Does not work if the script is not executable, or if it was
|
# Does not work if the script is not executable, or if it was
|
||||||
# started by searching $PATH.
|
# started by searching $PATH.
|
||||||
os.execv(sys.argv[0], sys.argv)
|
os.execv(sys.argv[0], sys.argv)
|
||||||
|
|
||||||
|
|
||||||
class MainMenu(Menu):
|
class MainMenu(Menu):
|
||||||
def special_input_choice(self, in_str):
|
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:
|
||||||
mv = in_str.split()
|
mv = in_str.split()
|
||||||
if len(mv) == 2 and mv[0].isdigit():
|
if len(mv) == 2 and mv[0].isdigit():
|
||||||
num = int(mv[0])
|
num = int(mv[0])
|
||||||
@@ -28,7 +30,7 @@ class MainMenu(Menu):
|
|||||||
else:
|
else:
|
||||||
num = 1
|
num = 1
|
||||||
item_name = in_str
|
item_name = in_str
|
||||||
buy_menu = BuyMenu(Session())
|
buy_menu = BuyMenu(self.sql_session)
|
||||||
thing = buy_menu.search_for_thing(item_name, find_hidden_products=False)
|
thing = buy_menu.search_for_thing(item_name, find_hidden_products=False)
|
||||||
if thing:
|
if thing:
|
||||||
buy_menu.execute(initial_contents=[(thing, num)])
|
buy_menu.execute(initial_contents=[(thing, num)])
|
||||||
@@ -36,32 +38,26 @@ class MainMenu(Menu):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def special_input_options(self, result):
|
def special_input_options(self, result: str) -> bool:
|
||||||
if result in faq_commands:
|
if result in faq_commands:
|
||||||
FAQMenu().execute()
|
FAQMenu(self.sql_session).execute()
|
||||||
return True
|
return True
|
||||||
if result in restart_commands:
|
if result in restart_commands:
|
||||||
if self.confirm("Restart Dibbler?"):
|
if self.confirm("Restart Dibbler?"):
|
||||||
restart()
|
restart()
|
||||||
pass
|
pass
|
||||||
return True
|
return True
|
||||||
elif result == "c":
|
if result == "c":
|
||||||
os.system(
|
print(f"\033[{random.randint(40, 49)};{random.randint(30, 37)};5m")
|
||||||
'echo -e "\033['
|
print("\033[2J")
|
||||||
+ str(random.randint(40, 49))
|
|
||||||
+ ";"
|
|
||||||
+ str(random.randint(30, 37))
|
|
||||||
+ ';5m"'
|
|
||||||
)
|
|
||||||
os.system("clear")
|
|
||||||
self.show_context()
|
self.show_context()
|
||||||
return True
|
return True
|
||||||
elif result == "cs":
|
if result == "cs":
|
||||||
os.system('echo -e "\033[0m"')
|
print("\033[0m")
|
||||||
os.system("clear")
|
print("\033[2J")
|
||||||
self.show_context()
|
self.show_context()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def invalid_menu_choice(self, in_str):
|
def invalid_menu_choice(self, in_str: str) -> None:
|
||||||
print(self.show_context())
|
print(self.show_context())
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.conf import config
|
from dibbler.conf import config
|
||||||
from dibbler.models import Transaction, Product, User
|
|
||||||
from dibbler.lib.helpers import less
|
from dibbler.lib.helpers import less
|
||||||
|
from dibbler.models import Product, Transaction, User
|
||||||
|
|
||||||
from .helpermenus import Menu, Selector
|
from .helpermenus import Menu, Selector
|
||||||
|
|
||||||
|
|
||||||
class TransferMenu(Menu):
|
class TransferMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Transfer credit between users", uses_db=True)
|
super().__init__("Transfer credit between users", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
amount = self.input_int("Transfer amount", 1, 100000)
|
amount = self.input_int("Transfer amount", 1, 100000)
|
||||||
self.set_context(f"Transferring {amount:d} kr", display=False)
|
self.set_context(f"Transferring {amount:d} kr", display=False)
|
||||||
@@ -26,24 +28,25 @@ class TransferMenu(Menu):
|
|||||||
t2 = Transaction(user2, -amount, f'transfer from {user1.name} "{comment}"')
|
t2 = Transaction(user2, -amount, f'transfer from {user1.name} "{comment}"')
|
||||||
t1.perform_transaction()
|
t1.perform_transaction()
|
||||||
t2.perform_transaction()
|
t2.perform_transaction()
|
||||||
self.session.add(t1)
|
self.sql_session.add(t1)
|
||||||
self.session.add(t2)
|
self.sql_session.add(t2)
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
print(f"Transferred {amount:d} kr from {user1} to {user2}")
|
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 {user1}'s credit is now {user1.credit:d} kr")
|
||||||
print(f"User {user2}'s credit is now {user2.credit:d} kr")
|
print(f"User {user2}'s credit is now {user2.credit:d} kr")
|
||||||
print(f"Comment: {comment}")
|
print(f"Comment: {comment}")
|
||||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not perform transfer: {e}")
|
print(f"Could not perform transfer: {e}")
|
||||||
# self.pause()
|
# self.pause()
|
||||||
|
|
||||||
|
|
||||||
class ShowUserMenu(Menu):
|
class ShowUserMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Show user", uses_db=True)
|
super().__init__("Show user", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
user = self.input_user("User name, card number or RFID")
|
user = self.input_user("User name, card number or RFID")
|
||||||
print(f"User name: {user.name}")
|
print(f"User name: {user.name}")
|
||||||
@@ -52,11 +55,12 @@ class ShowUserMenu(Menu):
|
|||||||
print(f"Credit: {user.credit} kr")
|
print(f"Credit: {user.credit} kr")
|
||||||
selector = Selector(
|
selector = Selector(
|
||||||
f"What do you want to know about {user.name}?",
|
f"What do you want to know about {user.name}?",
|
||||||
|
self.sql_session,
|
||||||
items=[
|
items=[
|
||||||
(
|
(
|
||||||
"transactions",
|
"transactions",
|
||||||
"Recent transactions (List of last "
|
"Recent transactions (List of last "
|
||||||
+ str(config.getint("limits", "user_recent_transaction_limit"))
|
+ str(config["limits"]["user_recent_transaction_limit"])
|
||||||
+ ")",
|
+ ")",
|
||||||
),
|
),
|
||||||
("products", f"Which products {user.name} has bought, and how many"),
|
("products", f"Which products {user.name} has bought, and how many"),
|
||||||
@@ -65,7 +69,7 @@ class ShowUserMenu(Menu):
|
|||||||
)
|
)
|
||||||
what = selector.execute()
|
what = selector.execute()
|
||||||
if what == "transactions":
|
if what == "transactions":
|
||||||
self.print_transactions(user, config.getint("limits", "user_recent_transaction_limit"))
|
self.print_transactions(user, config["limits"]["user_recent_transaction_limit"])
|
||||||
elif what == "products":
|
elif what == "products":
|
||||||
self.print_purchased_products(user)
|
self.print_purchased_products(user)
|
||||||
elif what == "transactions-all":
|
elif what == "transactions-all":
|
||||||
@@ -74,7 +78,7 @@ class ShowUserMenu(Menu):
|
|||||||
print("What what?")
|
print("What what?")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def print_transactions(user, limit=None):
|
def print_transactions(user: User, limit: int | None = None) -> None:
|
||||||
num_trans = len(user.transactions)
|
num_trans = len(user.transactions)
|
||||||
if limit is None:
|
if limit is None:
|
||||||
limit = num_trans
|
limit = num_trans
|
||||||
@@ -87,10 +91,7 @@ class ShowUserMenu(Menu):
|
|||||||
if t.purchase:
|
if t.purchase:
|
||||||
products = []
|
products = []
|
||||||
for entry in t.purchase.entries:
|
for entry in t.purchase.entries:
|
||||||
if abs(entry.amount) != 1:
|
amount = f"{abs(entry.amount)}x " if abs(entry.amount) != 1 else ""
|
||||||
amount = f"{abs(entry.amount)}x "
|
|
||||||
else:
|
|
||||||
amount = ""
|
|
||||||
product = f"{amount}{entry.product.name}"
|
product = f"{amount}{entry.product.name}"
|
||||||
products.append(product)
|
products.append(product)
|
||||||
string += "purchase ("
|
string += "purchase ("
|
||||||
@@ -98,13 +99,13 @@ class ShowUserMenu(Menu):
|
|||||||
string += ")"
|
string += ")"
|
||||||
if t.penalty > 1:
|
if t.penalty > 1:
|
||||||
string += f" * {t.penalty:d}x penalty applied"
|
string += f" * {t.penalty:d}x penalty applied"
|
||||||
else:
|
elif t.description is not None:
|
||||||
string += t.description
|
string += t.description
|
||||||
string += "\n"
|
string += "\n"
|
||||||
less(string)
|
less(string)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def print_purchased_products(user):
|
def print_purchased_products(user: User) -> None:
|
||||||
products = []
|
products = []
|
||||||
for ref in user.products:
|
for ref in user.products:
|
||||||
product = ref.product
|
product = ref.product
|
||||||
@@ -123,13 +124,13 @@ class ShowUserMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class UserListMenu(Menu):
|
class UserListMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "User list", uses_db=True)
|
super().__init__("User list", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
user_list = self.session.query(User).all()
|
user_list = self.sql_session.query(User).all()
|
||||||
total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0]
|
total_credit = self.sql_session.query(sqlalchemy.func.sum(User.credit)).first()[0]
|
||||||
|
|
||||||
line_format = "%-12s | %6s\n"
|
line_format = "%-12s | %6s\n"
|
||||||
hline = "---------------------\n"
|
hline = "---------------------\n"
|
||||||
@@ -144,10 +145,10 @@ class UserListMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class AdjustCreditMenu(Menu):
|
class AdjustCreditMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Adjust credit", uses_db=True)
|
super().__init__("Adjust credit", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
user = self.input_user("User")
|
user = self.input_user("User")
|
||||||
print(f"User {user.name}'s credit is {user.credit:d} kr")
|
print(f"User {user.name}'s credit is {user.credit:d} kr")
|
||||||
@@ -164,24 +165,25 @@ class AdjustCreditMenu(Menu):
|
|||||||
description = "manually adjusted credit"
|
description = "manually adjusted credit"
|
||||||
transaction = Transaction(user, -amount, description)
|
transaction = Transaction(user, -amount, description)
|
||||||
transaction.perform_transaction()
|
transaction.perform_transaction()
|
||||||
self.session.add(transaction)
|
self.sql_session.add(transaction)
|
||||||
try:
|
try:
|
||||||
self.session.commit()
|
self.sql_session.commit()
|
||||||
print(f"User {user.name}'s credit is now {user.credit:d} kr")
|
print(f"User {user.name}'s credit is now {user.credit:d} kr")
|
||||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
|
self.sql_session.rollback()
|
||||||
print(f"Could not store transaction: {e}")
|
print(f"Could not store transaction: {e}")
|
||||||
# self.pause()
|
# self.pause()
|
||||||
|
|
||||||
|
|
||||||
class ProductListMenu(Menu):
|
class ProductListMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Product list", uses_db=True)
|
super().__init__("Product list", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
text = ""
|
text = ""
|
||||||
product_list = (
|
product_list = (
|
||||||
self.session.query(Product)
|
self.sql_session.query(Product)
|
||||||
.filter(Product.hidden.is_(False))
|
.filter(Product.hidden.is_(False))
|
||||||
.order_by(Product.stock.desc())
|
.order_by(Product.stock.desc())
|
||||||
)
|
)
|
||||||
@@ -204,21 +206,22 @@ class ProductListMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class ProductSearchMenu(Menu):
|
class ProductSearchMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Product search", uses_db=True)
|
super().__init__("Product search", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
self.set_context("Enter (part of) product name or bar code")
|
self.set_context("Enter (part of) product name or bar code")
|
||||||
product = self.input_product()
|
product = self.input_product()
|
||||||
print(
|
print(
|
||||||
"Result: %s, price: %d kr, bar code: %s, stock: %d, hidden: %s"
|
", ".join(
|
||||||
% (
|
[
|
||||||
product.name,
|
f"Result: {product.name}",
|
||||||
product.price,
|
f"price: {product.price} kr",
|
||||||
product.bar_code,
|
f"bar code: {product.bar_code}",
|
||||||
product.stock,
|
f"stock: {product.stock}",
|
||||||
("Y" if product.hidden else "N"),
|
f"hidden: {'Y' if product.hidden else 'N'}",
|
||||||
)
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
# self.pause()
|
# self.pause()
|
||||||
|
|||||||
@@ -1,45 +1,46 @@
|
|||||||
import re
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.conf import config
|
|
||||||
from dibbler.models import Product, User
|
|
||||||
from dibbler.lib.printer_helpers import print_bar_code, print_name_label
|
|
||||||
|
|
||||||
|
# from dibbler.lib.printer_helpers import print_bar_code, print_name_label
|
||||||
from .helpermenus import Menu
|
from .helpermenus import Menu
|
||||||
|
|
||||||
|
|
||||||
class PrintLabelMenu(Menu):
|
class PrintLabelMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Print a label", uses_db=True)
|
super().__init__("Print a label", sql_session)
|
||||||
self.help_text = """
|
self.help_text = """
|
||||||
Prints out a product bar code on the printer
|
Prints out a product bar code on the printer
|
||||||
|
|
||||||
Put it up somewhere in the vicinity.
|
Put it up somewhere in the vicinity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
|
|
||||||
thing = self.input_thing("Product/User")
|
print("Printer menu is under renovation, please be patient")
|
||||||
|
|
||||||
if isinstance(thing, Product):
|
return
|
||||||
if re.match(r"^[0-9]{13}$", thing.bar_code):
|
|
||||||
bar_type = "ean13"
|
# thing = self.input_thing("Product/User")
|
||||||
elif re.match(r"^[0-9]{8}$", thing.bar_code):
|
|
||||||
bar_type = "ean8"
|
# if isinstance(thing, Product):
|
||||||
else:
|
# if re.match(r"^[0-9]{13}$", thing.bar_code):
|
||||||
bar_type = "code39"
|
# bar_type = "ean13"
|
||||||
print_bar_code(
|
# elif re.match(r"^[0-9]{8}$", thing.bar_code):
|
||||||
thing.bar_code,
|
# bar_type = "ean8"
|
||||||
thing.name,
|
# else:
|
||||||
barcode_type=bar_type,
|
# bar_type = "code39"
|
||||||
rotate=config.getboolean("printer", "rotate"),
|
# print_bar_code(
|
||||||
printer_type="QL-700",
|
# thing.bar_code,
|
||||||
label_type=config.get("printer", "label_type"),
|
# thing.name,
|
||||||
)
|
# barcode_type=bar_type,
|
||||||
elif isinstance(thing, User):
|
# rotate=config["printer"]["rotate"],
|
||||||
print_name_label(
|
# printer_type="QL-700",
|
||||||
text=thing.name,
|
# label_type=config.get("printer", "label_type"),
|
||||||
label_type=config.get("printer", "label_type"),
|
# )
|
||||||
rotate=config.getboolean("printer", "rotate"),
|
# elif isinstance(thing, User):
|
||||||
printer_type="QL-700",
|
# print_name_label(
|
||||||
)
|
# text=thing.name,
|
||||||
|
# label_type=config["printer"]["label_type"],
|
||||||
|
# rotate=config["printer"]["rotate"],
|
||||||
|
# printer_type="QL-700",
|
||||||
|
# )
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from sqlalchemy import desc, func
|
from sqlalchemy import desc, func
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.lib.helpers import less
|
from dibbler.lib.helpers import less
|
||||||
from dibbler.models import PurchaseEntry, Product, User
|
|
||||||
from dibbler.lib.statistikkHelpers import statisticsTextOnly
|
from dibbler.lib.statistikkHelpers import statisticsTextOnly
|
||||||
|
from dibbler.models import Product, PurchaseEntry, User
|
||||||
|
|
||||||
from .helpermenus import Menu
|
from .helpermenus import Menu
|
||||||
|
|
||||||
@@ -15,14 +16,14 @@ __all__ = [
|
|||||||
|
|
||||||
|
|
||||||
class ProductPopularityMenu(Menu):
|
class ProductPopularityMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Products by popularity", uses_db=True)
|
super().__init__("Products by popularity", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
text = ""
|
text = ""
|
||||||
sub = (
|
sub = (
|
||||||
self.session.query(
|
self.sql_session.query(
|
||||||
PurchaseEntry.product_id,
|
PurchaseEntry.product_id,
|
||||||
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
||||||
)
|
)
|
||||||
@@ -31,8 +32,8 @@ class ProductPopularityMenu(Menu):
|
|||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
product_list = (
|
product_list = (
|
||||||
self.session.query(Product, sub.c.purchase_count)
|
self.sql_session.query(Product, sub.c.purchase_count)
|
||||||
.outerjoin((sub, Product.product_id == sub.c.product_id))
|
.outerjoin(sub, Product.product_id == sub.c.product_id)
|
||||||
.order_by(desc(sub.c.purchase_count))
|
.order_by(desc(sub.c.purchase_count))
|
||||||
.filter(sub.c.purchase_count is not None)
|
.filter(sub.c.purchase_count is not None)
|
||||||
.all()
|
.all()
|
||||||
@@ -48,14 +49,14 @@ class ProductPopularityMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class ProductRevenueMenu(Menu):
|
class ProductRevenueMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Products by revenue", uses_db=True)
|
super().__init__("Products by revenue", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
text = ""
|
text = ""
|
||||||
sub = (
|
sub = (
|
||||||
self.session.query(
|
self.sql_session.query(
|
||||||
PurchaseEntry.product_id,
|
PurchaseEntry.product_id,
|
||||||
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
||||||
)
|
)
|
||||||
@@ -64,8 +65,8 @@ class ProductRevenueMenu(Menu):
|
|||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
product_list = (
|
product_list = (
|
||||||
self.session.query(Product, sub.c.purchase_count)
|
self.sql_session.query(Product, sub.c.purchase_count)
|
||||||
.outerjoin((sub, Product.product_id == sub.c.product_id))
|
.outerjoin(sub, Product.product_id == sub.c.product_id)
|
||||||
.order_by(desc(sub.c.purchase_count * Product.price))
|
.order_by(desc(sub.c.purchase_count * Product.price))
|
||||||
.filter(sub.c.purchase_count is not None)
|
.filter(sub.c.purchase_count is not None)
|
||||||
.all()
|
.all()
|
||||||
@@ -86,22 +87,26 @@ class ProductRevenueMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class BalanceMenu(Menu):
|
class BalanceMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Total balance of PVVVV", uses_db=True)
|
super().__init__("Total balance of PVVVV", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
text = ""
|
text = ""
|
||||||
total_value = 0
|
total_value = 0
|
||||||
product_list = self.session.query(Product).filter(Product.stock > 0).all()
|
product_list = self.sql_session.query(Product).filter(Product.stock > 0).all()
|
||||||
for p in product_list:
|
for p in product_list:
|
||||||
total_value += p.stock * p.price
|
total_value += p.stock * p.price
|
||||||
|
|
||||||
total_positive_credit = (
|
total_positive_credit = (
|
||||||
self.session.query(func.sum(User.credit)).filter(User.credit > 0).first()[0]
|
self.sql_session.query(func.coalesce(func.sum(User.credit), 0))
|
||||||
|
.filter(User.credit > 0)
|
||||||
|
.first()[0]
|
||||||
)
|
)
|
||||||
total_negative_credit = (
|
total_negative_credit = (
|
||||||
self.session.query(func.sum(User.credit)).filter(User.credit < 0).first()[0]
|
self.sql_session.query(func.coalesce(func.sum(User.credit), 0))
|
||||||
|
.filter(User.credit < 0)
|
||||||
|
.first()[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
total_credit = total_positive_credit + total_negative_credit
|
total_credit = total_positive_credit + total_negative_credit
|
||||||
@@ -119,8 +124,8 @@ class BalanceMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class LoggedStatisticsMenu(Menu):
|
class LoggedStatisticsMenu(Menu):
|
||||||
def __init__(self):
|
def __init__(self, sql_session: Session) -> None:
|
||||||
Menu.__init__(self, "Statistics from log", uses_db=True)
|
super().__init__("Statistics from log", sql_session)
|
||||||
|
|
||||||
def _execute(self):
|
def _execute(self, **_kwargs) -> None:
|
||||||
statisticsTextOnly()
|
statisticsTextOnly(self.sql_session)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Base(DeclarativeBase):
|
|||||||
"ck": "ck_%(table_name)s_`%(constraint_name)s`",
|
"ck": "ck_%(table_name)s_`%(constraint_name)s`",
|
||||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||||
"pk": "pk_%(table_name)s",
|
"pk": "pk_%(table_name)s",
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@declared_attr.directive
|
@declared_attr.directive
|
||||||
@@ -38,7 +38,7 @@ class Base(DeclarativeBase):
|
|||||||
isinstance(v, InstrumentedList),
|
isinstance(v, InstrumentedList),
|
||||||
isinstance(v, InstrumentedSet),
|
isinstance(v, InstrumentedSet),
|
||||||
isinstance(v, InstrumentedDict),
|
isinstance(v, InstrumentedDict),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return f"<{self.__class__.__name__}({columns})>"
|
return f"<{self.__class__.__name__}({columns})>"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
@@ -36,12 +37,19 @@ class Product(Base):
|
|||||||
name_re = r".+"
|
name_re = r".+"
|
||||||
name_length = 45
|
name_length = 45
|
||||||
|
|
||||||
def __init__(self, bar_code, name, price, stock=0, hidden=False):
|
def __init__(
|
||||||
|
self,
|
||||||
|
bar_code: str,
|
||||||
|
name: str,
|
||||||
|
price: int,
|
||||||
|
stock: int = 0,
|
||||||
|
hidden: bool = False,
|
||||||
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.bar_code = bar_code
|
self.bar_code = bar_code
|
||||||
self.price = price
|
self.price = price
|
||||||
self.stock = stock
|
self.stock = stock
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
import math
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
DateTime,
|
DateTime,
|
||||||
@@ -29,23 +29,23 @@ class Purchase(Base):
|
|||||||
price: Mapped[int] = mapped_column(Integer)
|
price: Mapped[int] = mapped_column(Integer)
|
||||||
|
|
||||||
transactions: Mapped[set[Transaction]] = relationship(
|
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")
|
entries: Mapped[set[PurchaseEntry]] = relationship(back_populates="purchase")
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def is_complete(self):
|
def is_complete(self) -> bool:
|
||||||
return len(self.transactions) > 0 and len(self.entries) > 0
|
return len(self.transactions) > 0 and len(self.entries) > 0
|
||||||
|
|
||||||
def price_per_transaction(self, round_up=True):
|
def price_per_transaction(self, round_up: bool = True) -> int:
|
||||||
if round_up:
|
if round_up:
|
||||||
return int(math.ceil(float(self.price) / len(self.transactions)))
|
return int(math.ceil(float(self.price) / len(self.transactions)))
|
||||||
else:
|
|
||||||
return int(math.floor(float(self.price) / len(self.transactions)))
|
return int(math.floor(float(self.price) / len(self.transactions)))
|
||||||
|
|
||||||
def set_price(self, round_up=True):
|
def set_price(self, round_up: bool = True) -> None:
|
||||||
self.price = 0
|
self.price = 0
|
||||||
for entry in self.entries:
|
for entry in self.entries:
|
||||||
self.price += entry.amount * entry.product.price
|
self.price += entry.amount * entry.product.price
|
||||||
@@ -53,16 +53,16 @@ class Purchase(Base):
|
|||||||
for t in self.transactions:
|
for t in self.transactions:
|
||||||
t.amount = self.price_per_transaction(round_up=round_up)
|
t.amount = self.price_per_transaction(round_up=round_up)
|
||||||
|
|
||||||
def perform_purchase(self, ignore_penalty=False, round_up=True):
|
def perform_purchase(self, ignore_penalty: bool = False, round_up: bool = True) -> None:
|
||||||
self.time = datetime.datetime.now()
|
self.time = datetime.now()
|
||||||
self.set_price(round_up=round_up)
|
self.set_price(round_up=round_up)
|
||||||
for t in self.transactions:
|
for t in self.transactions:
|
||||||
t.perform_transaction(ignore_penalty=ignore_penalty)
|
t.perform_transaction(ignore_penalty=ignore_penalty)
|
||||||
for entry in self.entries:
|
for entry in self.entries:
|
||||||
entry.product.stock -= entry.amount
|
entry.product.stock -= entry.amount
|
||||||
|
|
||||||
def perform_soft_purchase(self, price, round_up=True):
|
def perform_soft_purchase(self, price: int, round_up: bool = True) -> None:
|
||||||
self.time = datetime.datetime.now()
|
self.time = datetime.now()
|
||||||
self.price = price
|
self.price = price
|
||||||
for t in self.transactions:
|
for t in self.transactions:
|
||||||
t.amount = self.price_per_transaction(round_up=round_up)
|
t.amount = self.price_per_transaction(round_up=round_up)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Integer,
|
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
@@ -27,10 +28,15 @@ class PurchaseEntry(Base):
|
|||||||
product_id: Mapped[int] = mapped_column(ForeignKey("products.product_id"))
|
product_id: Mapped[int] = mapped_column(ForeignKey("products.product_id"))
|
||||||
purchase_id: Mapped[int] = mapped_column(ForeignKey("purchases.id"))
|
purchase_id: Mapped[int] = mapped_column(ForeignKey("purchases.id"))
|
||||||
|
|
||||||
product: Mapped[Product] = relationship(lazy="joined")
|
product: Mapped[Product] = relationship(back_populates="purchases", lazy="joined")
|
||||||
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
purchase: Mapped[Purchase] = relationship(back_populates="entries", lazy="joined")
|
||||||
|
|
||||||
def __init__(self, purchase, product, amount):
|
def __init__(
|
||||||
|
self,
|
||||||
|
purchase: Purchase,
|
||||||
|
product: Product,
|
||||||
|
amount: int,
|
||||||
|
) -> None:
|
||||||
self.product = product
|
self.product = product
|
||||||
self.product_bar_code = product.bar_code
|
self.product_bar_code = product.bar_code
|
||||||
self.purchase = purchase
|
self.purchase = purchase
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
DateTime,
|
DateTime,
|
||||||
@@ -18,8 +18,8 @@ from sqlalchemy.orm import (
|
|||||||
from .Base import Base
|
from .Base import Base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .User import User
|
|
||||||
from .Purchase import Purchase
|
from .Purchase import Purchase
|
||||||
|
from .User import User
|
||||||
|
|
||||||
|
|
||||||
class Transaction(Base):
|
class Transaction(Base):
|
||||||
@@ -36,17 +36,24 @@ class Transaction(Base):
|
|||||||
purchase_id: Mapped[int | None] = mapped_column(ForeignKey("purchases.id"))
|
purchase_id: Mapped[int | None] = mapped_column(ForeignKey("purchases.id"))
|
||||||
|
|
||||||
user: Mapped[User] = relationship(lazy="joined")
|
user: Mapped[User] = relationship(lazy="joined")
|
||||||
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
purchase: Mapped[Purchase] = relationship(back_populates="transactions", lazy="joined")
|
||||||
|
|
||||||
def __init__(self, user, amount=0, description=None, purchase=None, penalty=1):
|
def __init__(
|
||||||
|
self,
|
||||||
|
user: User,
|
||||||
|
amount: int = 0,
|
||||||
|
description: str | None = None,
|
||||||
|
purchase: Purchase | None = None,
|
||||||
|
penalty: int = 1,
|
||||||
|
) -> None:
|
||||||
self.user = user
|
self.user = user
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
self.description = description
|
self.description = description
|
||||||
self.purchase = purchase
|
self.purchase = purchase
|
||||||
self.penalty = penalty
|
self.penalty = penalty
|
||||||
|
|
||||||
def perform_transaction(self, ignore_penalty=False):
|
def perform_transaction(self, ignore_penalty: bool = False) -> None:
|
||||||
self.time = datetime.datetime.now()
|
self.time = datetime.now()
|
||||||
if not ignore_penalty:
|
if not ignore_penalty:
|
||||||
self.amount *= self.penalty
|
self.amount *= self.penalty
|
||||||
self.user.credit -= self.amount
|
self.user.credit -= self.amount
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
@@ -14,25 +15,34 @@ from sqlalchemy.orm import (
|
|||||||
from .Base import Base
|
from .Base import Base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .UserProducts import UserProducts
|
|
||||||
from .Transaction import Transaction
|
from .Transaction import Transaction
|
||||||
|
from .UserProducts import UserProducts
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
name: Mapped[str] = mapped_column(String(10), primary_key=True)
|
name: Mapped[str] = mapped_column(String(10), primary_key=True)
|
||||||
credit: Mapped[str] = mapped_column(Integer)
|
credit: Mapped[int] = mapped_column(Integer)
|
||||||
card: Mapped[str | None] = mapped_column(String(20))
|
card: Mapped[str | None] = mapped_column(String(20))
|
||||||
rfid: Mapped[str | None] = mapped_column(String(20))
|
rfid: Mapped[str | None] = mapped_column(String(20))
|
||||||
|
|
||||||
products: Mapped[set[UserProducts]] = relationship(back_populates="user")
|
products: Mapped[list[UserProducts]] = relationship(back_populates="user")
|
||||||
transactions: Mapped[set[Transaction]] = relationship(back_populates="user")
|
transactions: Mapped[list[Transaction]] = relationship(
|
||||||
|
back_populates="user",
|
||||||
|
order_by="Transaction.time",
|
||||||
|
)
|
||||||
|
|
||||||
name_re = r"[a-z]+"
|
name_re = r"[a-z]+"
|
||||||
card_re = r"(([Nn][Tt][Nn][Uu])?[0-9]+)?"
|
card_re = r"(([Nn][Tt][Nn][Uu])?[0-9]+)?"
|
||||||
rfid_re = r"[0-9a-fA-F]*"
|
rfid_re = r"[0-9a-fA-F]*"
|
||||||
|
|
||||||
def __init__(self, name, card, rfid=None, credit=0):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
card: str | None,
|
||||||
|
rfid: str | None = None,
|
||||||
|
credit: int = 0,
|
||||||
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
if card == "":
|
if card == "":
|
||||||
card = None
|
card = None
|
||||||
@@ -42,8 +52,8 @@ class User(Base):
|
|||||||
self.rfid = rfid
|
self.rfid = rfid
|
||||||
self.credit = credit
|
self.credit = credit
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def is_anonymous(self):
|
def is_anonymous(self) -> bool:
|
||||||
return self.card == "11122233"
|
return self.card == "11122233"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Integer,
|
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
@@ -14,8 +15,8 @@ from sqlalchemy.orm import (
|
|||||||
from .Base import Base
|
from .Base import Base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .User import User
|
|
||||||
from .Product import Product
|
from .Product import Product
|
||||||
|
from .User import User
|
||||||
|
|
||||||
|
|
||||||
class UserProducts(Base):
|
class UserProducts(Base):
|
||||||
@@ -27,5 +28,5 @@ class UserProducts(Base):
|
|||||||
count: Mapped[int] = mapped_column(Integer)
|
count: Mapped[int] = mapped_column(Integer)
|
||||||
sign: Mapped[int] = mapped_column(Integer)
|
sign: Mapped[int] = mapped_column(Integer)
|
||||||
|
|
||||||
user: Mapped[User] = relationship()
|
user: Mapped[User] = relationship(back_populates="products")
|
||||||
product: Mapped[Product] = relationship()
|
product: Mapped[Product] = relationship(back_populates="users")
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'Base',
|
"Base",
|
||||||
'Product',
|
"Product",
|
||||||
'Purchase',
|
"Purchase",
|
||||||
'PurchaseEntry',
|
"PurchaseEntry",
|
||||||
'Transaction',
|
"Transaction",
|
||||||
'User',
|
"User",
|
||||||
'UserProducts',
|
"UserProducts",
|
||||||
]
|
]
|
||||||
|
|
||||||
from .Base import Base
|
from .Base import Base
|
||||||
|
|||||||
@@ -1,79 +1,111 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
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 ..conf import config
|
||||||
from ..lib.helpers import *
|
from ..menus import (
|
||||||
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,
|
||||||
|
)
|
||||||
|
|
||||||
random.seed()
|
random.seed()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(sql_session: Session) -> None:
|
||||||
if not config.getboolean("general", "stop_allowed"):
|
if not config["general"]["stop_allowed"]:
|
||||||
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
|
set_signal_handler(SIGQUIT, SIG_IGN)
|
||||||
|
|
||||||
if not config.getboolean("general", "stop_allowed"):
|
if not config["general"]["stop_allowed"]:
|
||||||
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
|
set_signal_handler(SIGTSTP, SIG_IGN)
|
||||||
|
|
||||||
main = MainMenu(
|
main_menu = MainMenu(
|
||||||
"Dibbler main menu",
|
sql_session,
|
||||||
items=[
|
items=[
|
||||||
BuyMenu(),
|
BuyMenu(sql_session),
|
||||||
ProductListMenu(),
|
ProductListMenu(sql_session),
|
||||||
ShowUserMenu(),
|
ShowUserMenu(sql_session),
|
||||||
UserListMenu(),
|
UserListMenu(sql_session),
|
||||||
AdjustCreditMenu(),
|
AdjustCreditMenu(sql_session),
|
||||||
TransferMenu(),
|
TransferMenu(sql_session),
|
||||||
AddStockMenu(),
|
AddStockMenu(sql_session),
|
||||||
Menu(
|
Menu(
|
||||||
"Add/edit",
|
"Add/edit",
|
||||||
|
sql_session,
|
||||||
items=[
|
items=[
|
||||||
AddUserMenu(),
|
AddUserMenu(sql_session),
|
||||||
EditUserMenu(),
|
EditUserMenu(sql_session),
|
||||||
AddProductMenu(),
|
AddProductMenu(sql_session),
|
||||||
EditProductMenu(),
|
EditProductMenu(sql_session),
|
||||||
AdjustStockMenu(),
|
AdjustStockMenu(sql_session),
|
||||||
CleanupStockMenu(),
|
CleanupStockMenu(sql_session),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ProductSearchMenu(),
|
ProductSearchMenu(sql_session),
|
||||||
Menu(
|
Menu(
|
||||||
"Statistics",
|
"Statistics",
|
||||||
|
sql_session,
|
||||||
items=[
|
items=[
|
||||||
ProductPopularityMenu(),
|
ProductPopularityMenu(sql_session),
|
||||||
ProductRevenueMenu(),
|
ProductRevenueMenu(sql_session),
|
||||||
BalanceMenu(),
|
BalanceMenu(sql_session),
|
||||||
LoggedStatisticsMenu(),
|
LoggedStatisticsMenu(sql_session),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
FAQMenu(),
|
FAQMenu(sql_session),
|
||||||
PrintLabelMenu(),
|
PrintLabelMenu(sql_session),
|
||||||
],
|
],
|
||||||
exit_msg="happy happy joy joy",
|
exit_msg="happy happy joy joy",
|
||||||
exit_confirm_msg="Really quit Dibbler?",
|
exit_confirm_msg="Really quit Dibbler?",
|
||||||
)
|
)
|
||||||
if not config.getboolean("general", "quit_allowed"):
|
if not config["general"]["quit_allowed"]:
|
||||||
main.exit_disallowed_msg = "You can check out any time you like, but you can never leave."
|
main_menu.exit_disallowed_msg = (
|
||||||
|
"You can check out any time you like, but you can never leave."
|
||||||
|
)
|
||||||
while True:
|
while True:
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
main.execute()
|
main_menu.execute()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("")
|
print("")
|
||||||
print("Interrupted.")
|
print("Interrupted.")
|
||||||
except:
|
except:
|
||||||
print("Something went wrong.")
|
print("Something went wrong.")
|
||||||
print(f"{sys.exc_info()[0]}: {sys.exc_info()[1]}")
|
print(f"{sys.exc_info()[0]}: {sys.exc_info()[1]}")
|
||||||
if config.getboolean("general", "show_tracebacks"):
|
if config["general"]["show_tracebacks"]:
|
||||||
traceback.print_tb(sys.exc_info()[2])
|
traceback.print_tb(sys.exc_info()[2])
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
print("Restarting main menu.")
|
print("Restarting main menu.")
|
||||||
|
main_menu.sql_session.reset()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
|
||||||
from dibbler.models import Base
|
from dibbler.models import Base
|
||||||
from dibbler.db import engine
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(engine: Engine) -> None:
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|||||||
46
dibbler/subcommands/seed_test_data.py
Normal file
46
dibbler/subcommands/seed_test_data.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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,18 +1,13 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
|
||||||
from dibbler.db import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from dibbler.models import User
|
from dibbler.models import User
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(sql_session: Session) -> None:
|
||||||
# Start an SQL session
|
|
||||||
session = Session()
|
|
||||||
# Let's find all users with a negative credit
|
# Let's find all users with a negative credit
|
||||||
slabbedasker = session.query(User).filter(User.credit < 0).all()
|
slabbedasker = sql_session.query(User).filter(User.credit < 0).all()
|
||||||
|
|
||||||
for slubbert in slabbedasker:
|
for slubbert in slabbedasker:
|
||||||
print(f"{slubbert.name}, {slubbert.credit}")
|
print(f"{slubbert.name}, {slubbert.credit}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|||||||
@@ -1,231 +1,231 @@
|
|||||||
#! /usr/bin/env python
|
# #! /usr/bin/env python
|
||||||
|
|
||||||
# TODO: fixme
|
# # TODO: fixme
|
||||||
|
|
||||||
# -*- coding: UTF-8 -*-
|
# # -*- coding: UTF-8 -*-
|
||||||
import matplotlib.pyplot as plt
|
# import matplotlib.pyplot as plt
|
||||||
import matplotlib.dates as mdates
|
# import matplotlib.dates as mdates
|
||||||
|
|
||||||
from dibbler.lib.statistikkHelpers import *
|
# from dibbler.lib.statistikkHelpers import *
|
||||||
|
|
||||||
|
|
||||||
def getInputType():
|
# def getInputType():
|
||||||
inp = 0
|
# inp = 0
|
||||||
while not (inp == "1" or inp == "2" or inp == "3" or inp == "4"):
|
# while not (inp == "1" or inp == "2" or inp == "3" or inp == "4"):
|
||||||
print("type 1 for user-statistics")
|
# print("type 1 for user-statistics")
|
||||||
print("type 2 for product-statistics")
|
# print("type 2 for product-statistics")
|
||||||
print("type 3 for global-statistics")
|
# print("type 3 for global-statistics")
|
||||||
print("type 4 to enter loop-mode")
|
# print("type 4 to enter loop-mode")
|
||||||
inp = input("")
|
# inp = input("")
|
||||||
return int(inp)
|
# return int(inp)
|
||||||
|
|
||||||
|
|
||||||
def getDateFile(date, n):
|
# def getDateFile(date, n):
|
||||||
try:
|
# try:
|
||||||
if n == 0:
|
# if n == 0:
|
||||||
inp = input("start date? (yyyy-mm-dd) ")
|
# inp = input("start date? (yyyy-mm-dd) ")
|
||||||
elif n == -1:
|
# elif n == -1:
|
||||||
inp = input("end date? (yyyy-mm-dd) ")
|
# inp = input("end date? (yyyy-mm-dd) ")
|
||||||
year = inp.partition("-")
|
# year = inp.partition("-")
|
||||||
month = year[2].partition("-")
|
# month = year[2].partition("-")
|
||||||
return datetime.date(int(year[0]), int(month[0]), int(month[2]))
|
# return datetime.date(int(year[0]), int(month[0]), int(month[2]))
|
||||||
except:
|
# except:
|
||||||
print("invalid date, setting start start date")
|
# print("invalid date, setting start start date")
|
||||||
if n == 0:
|
# if n == 0:
|
||||||
print("to date found on first line")
|
# print("to date found on first line")
|
||||||
elif n == -1:
|
# elif n == -1:
|
||||||
print("to date found on last line")
|
# print("to date found on last line")
|
||||||
print(date)
|
# print(date)
|
||||||
return datetime.date(
|
# return datetime.date(
|
||||||
int(date.partition("-")[0]),
|
# int(date.partition("-")[0]),
|
||||||
int(date.partition("-")[2].partition("-")[0]),
|
# int(date.partition("-")[2].partition("-")[0]),
|
||||||
int(date.partition("-")[2].partition("-")[2]),
|
# int(date.partition("-")[2].partition("-")[2]),
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def dateToDateNumFile(date, startDate):
|
# def dateToDateNumFile(date, startDate):
|
||||||
year = date.partition("-")
|
# year = date.partition("-")
|
||||||
month = year[2].partition("-")
|
# month = year[2].partition("-")
|
||||||
day = datetime.date(int(year[0]), int(month[0]), int(month[2]))
|
# day = datetime.date(int(year[0]), int(month[0]), int(month[2]))
|
||||||
deltaDays = day - startDate
|
# deltaDays = day - startDate
|
||||||
return int(deltaDays.days), day.weekday()
|
# return int(deltaDays.days), day.weekday()
|
||||||
|
|
||||||
|
|
||||||
def getProducts(products):
|
# def getProducts(products):
|
||||||
product = []
|
# product = []
|
||||||
products = products.partition("¤")
|
# products = products.partition("¤")
|
||||||
product.append(products[0])
|
# product.append(products[0])
|
||||||
while products[1] == "¤":
|
# while products[1] == "¤":
|
||||||
products = products[2].partition("¤")
|
# products = products[2].partition("¤")
|
||||||
product.append(products[0])
|
# product.append(products[0])
|
||||||
return product
|
# return product
|
||||||
|
|
||||||
|
|
||||||
def piePlot(dictionary, n):
|
# def piePlot(dictionary, n):
|
||||||
keys = []
|
# keys = []
|
||||||
values = []
|
# values = []
|
||||||
i = 0
|
# i = 0
|
||||||
for key in sorted(dictionary, key=dictionary.get, reverse=True):
|
# for key in sorted(dictionary, key=dictionary.get, reverse=True):
|
||||||
values.append(dictionary[key])
|
# values.append(dictionary[key])
|
||||||
if i < n:
|
# if i < n:
|
||||||
keys.append(key)
|
# keys.append(key)
|
||||||
i += 1
|
# i += 1
|
||||||
else:
|
# else:
|
||||||
keys.append("")
|
# keys.append("")
|
||||||
plt.pie(values, labels=keys)
|
# plt.pie(values, labels=keys)
|
||||||
|
|
||||||
|
|
||||||
def datePlot(array, dateLine):
|
# def datePlot(array, dateLine):
|
||||||
if not array == []:
|
# if not array == []:
|
||||||
plt.bar(dateLine, array)
|
# plt.bar(dateLine, array)
|
||||||
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%b"))
|
# plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%b"))
|
||||||
|
|
||||||
|
|
||||||
def dayPlot(array, days):
|
# def dayPlot(array, days):
|
||||||
if not array == []:
|
# if not array == []:
|
||||||
for i in range(7):
|
# for i in range(7):
|
||||||
array[i] = array[i] * 7.0 / days
|
# array[i] = array[i] * 7.0 / days
|
||||||
plt.bar(list(range(7)), array)
|
# plt.bar(list(range(7)), array)
|
||||||
plt.xticks(
|
# plt.xticks(
|
||||||
list(range(7)),
|
# list(range(7)),
|
||||||
[
|
# [
|
||||||
" mon",
|
# " mon",
|
||||||
" tue",
|
# " tue",
|
||||||
" wed",
|
# " wed",
|
||||||
" thu",
|
# " thu",
|
||||||
" fri",
|
# " fri",
|
||||||
" sat",
|
# " sat",
|
||||||
" sun",
|
# " sun",
|
||||||
],
|
# ],
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def graphPlot(array, dateLine):
|
# def graphPlot(array, dateLine):
|
||||||
if not array == []:
|
# if not array == []:
|
||||||
plt.plot(dateLine, array)
|
# plt.plot(dateLine, array)
|
||||||
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%b"))
|
# plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%b"))
|
||||||
|
|
||||||
|
|
||||||
def plotUser(database, dateLine, user, n):
|
# def plotUser(database, dateLine, user, n):
|
||||||
printUser(database, dateLine, user, n)
|
# printUser(database, dateLine, user, n)
|
||||||
plt.subplot(221)
|
# plt.subplot(221)
|
||||||
piePlot(database.personVareAntall[user], n)
|
# piePlot(database.personVareAntall[user], n)
|
||||||
plt.xlabel("antall varer kjøpt gjengitt i antall")
|
# plt.xlabel("antall varer kjøpt gjengitt i antall")
|
||||||
plt.subplot(222)
|
# plt.subplot(222)
|
||||||
datePlot(database.personDatoVerdi[user], dateLine)
|
# datePlot(database.personDatoVerdi[user], dateLine)
|
||||||
plt.xlabel("penger brukt over dato")
|
# plt.xlabel("penger brukt over dato")
|
||||||
plt.subplot(223)
|
# plt.subplot(223)
|
||||||
piePlot(database.personVareVerdi[user], n)
|
# piePlot(database.personVareVerdi[user], n)
|
||||||
plt.xlabel("antall varer kjøpt gjengitt i verdi")
|
# plt.xlabel("antall varer kjøpt gjengitt i verdi")
|
||||||
plt.subplot(224)
|
# plt.subplot(224)
|
||||||
dayPlot(database.personUkedagVerdi[user], len(dateLine))
|
# dayPlot(database.personUkedagVerdi[user], len(dateLine))
|
||||||
plt.xlabel("forbruk over ukedager")
|
# plt.xlabel("forbruk over ukedager")
|
||||||
plt.show()
|
# plt.show()
|
||||||
|
|
||||||
|
|
||||||
def plotProduct(database, dateLine, product, n):
|
# def plotProduct(database, dateLine, product, n):
|
||||||
printProduct(database, dateLine, product, n)
|
# printProduct(database, dateLine, product, n)
|
||||||
plt.subplot(221)
|
# plt.subplot(221)
|
||||||
piePlot(database.varePersonAntall[product], n)
|
# piePlot(database.varePersonAntall[product], n)
|
||||||
plt.xlabel("personer som har handler produktet")
|
# plt.xlabel("personer som har handler produktet")
|
||||||
plt.subplot(222)
|
# plt.subplot(222)
|
||||||
datePlot(database.vareDatoAntall[product], dateLine)
|
# datePlot(database.vareDatoAntall[product], dateLine)
|
||||||
plt.xlabel("antall produkter handlet per dag")
|
# plt.xlabel("antall produkter handlet per dag")
|
||||||
# plt.subplot(223)
|
# # plt.subplot(223)
|
||||||
plt.subplot(224)
|
# plt.subplot(224)
|
||||||
dayPlot(database.vareUkedagAntall[product], len(dateLine))
|
# dayPlot(database.vareUkedagAntall[product], len(dateLine))
|
||||||
plt.xlabel("antall over ukedager")
|
# plt.xlabel("antall over ukedager")
|
||||||
plt.show()
|
# plt.show()
|
||||||
|
|
||||||
|
|
||||||
def plotGlobal(database, dateLine, n):
|
# def plotGlobal(database, dateLine, n):
|
||||||
printGlobal(database, dateLine, n)
|
# printGlobal(database, dateLine, n)
|
||||||
plt.subplot(231)
|
# plt.subplot(231)
|
||||||
piePlot(database.globalVareVerdi, n)
|
# piePlot(database.globalVareVerdi, n)
|
||||||
plt.xlabel("varer kjøpt gjengitt som verdi")
|
# plt.xlabel("varer kjøpt gjengitt som verdi")
|
||||||
plt.subplot(232)
|
# plt.subplot(232)
|
||||||
datePlot(database.globalDatoForbruk, dateLine)
|
# datePlot(database.globalDatoForbruk, dateLine)
|
||||||
plt.xlabel("forbruk over dato")
|
# plt.xlabel("forbruk over dato")
|
||||||
plt.subplot(233)
|
# plt.subplot(233)
|
||||||
graphPlot(database.pengebeholdning, dateLine)
|
# graphPlot(database.pengebeholdning, dateLine)
|
||||||
plt.xlabel("pengebeholdning over tid (negativ verdi utgjør samlet kreditt)")
|
# plt.xlabel("pengebeholdning over tid (negativ verdi utgjør samlet kreditt)")
|
||||||
plt.subplot(234)
|
# plt.subplot(234)
|
||||||
piePlot(database.globalPersonForbruk, n)
|
# piePlot(database.globalPersonForbruk, n)
|
||||||
plt.xlabel("penger brukt av personer")
|
# plt.xlabel("penger brukt av personer")
|
||||||
plt.subplot(235)
|
# plt.subplot(235)
|
||||||
dayPlot(database.globalUkedagForbruk, len(dateLine))
|
# dayPlot(database.globalUkedagForbruk, len(dateLine))
|
||||||
plt.xlabel("forbruk over ukedager")
|
# plt.xlabel("forbruk over ukedager")
|
||||||
plt.show()
|
# plt.show()
|
||||||
|
|
||||||
|
|
||||||
def alt4menu(database, dateLine, useDatabase):
|
# def alt4menu(database, dateLine, useDatabase):
|
||||||
n = 10
|
# n = 10
|
||||||
while 1:
|
# while 1:
|
||||||
print(
|
# 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"
|
||||||
)
|
# )
|
||||||
try:
|
# try:
|
||||||
inp = input("")
|
# inp = input("")
|
||||||
except:
|
# except:
|
||||||
continue
|
# continue
|
||||||
if inp == "q":
|
# if inp == "q":
|
||||||
break
|
# break
|
||||||
elif inp == "1":
|
# elif inp == "1":
|
||||||
if i == "0":
|
# if i == "0":
|
||||||
user = input("input full username: ")
|
# user = input("input full username: ")
|
||||||
else:
|
# else:
|
||||||
user = getUser()
|
# user = getUser()
|
||||||
plotUser(database, dateLine, user, n)
|
# plotUser(database, dateLine, user, n)
|
||||||
elif inp == "2":
|
# elif inp == "2":
|
||||||
if i == "0":
|
# if i == "0":
|
||||||
product = input("input full product name: ")
|
# product = input("input full product name: ")
|
||||||
else:
|
# else:
|
||||||
product = getProduct()
|
# product = getProduct()
|
||||||
plotProduct(database, dateLine, product, n)
|
# plotProduct(database, dateLine, product, n)
|
||||||
elif inp == "3":
|
# elif inp == "3":
|
||||||
plotGlobal(database, dateLine, n)
|
# plotGlobal(database, dateLine, n)
|
||||||
elif inp == "n":
|
# elif inp == "n":
|
||||||
try:
|
# try:
|
||||||
n = int(input("set number to show "))
|
# n = int(input("set number to show "))
|
||||||
except:
|
# except:
|
||||||
pass
|
# pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
# def main():
|
||||||
inputType = getInputType()
|
# inputType = getInputType()
|
||||||
i = input("0:fil, 1:database \n? ")
|
# i = input("0:fil, 1:database \n? ")
|
||||||
if inputType == 1:
|
# if inputType == 1:
|
||||||
if i == "0":
|
# if i == "0":
|
||||||
user = input("input full username: ")
|
# user = input("input full username: ")
|
||||||
else:
|
# else:
|
||||||
user = getUser()
|
# user = getUser()
|
||||||
product = ""
|
# product = ""
|
||||||
elif inputType == 2:
|
# elif inputType == 2:
|
||||||
if i == "0":
|
# if i == "0":
|
||||||
product = input("input full product name: ")
|
# product = input("input full product name: ")
|
||||||
else:
|
# else:
|
||||||
product = getProduct()
|
# product = getProduct()
|
||||||
user = ""
|
# user = ""
|
||||||
else:
|
# else:
|
||||||
product = ""
|
# product = ""
|
||||||
user = ""
|
# user = ""
|
||||||
if i == "0":
|
# if i == "0":
|
||||||
inputFile = input("logfil? ")
|
# inputFile = input("logfil? ")
|
||||||
if inputFile == "":
|
# if inputFile == "":
|
||||||
inputFile = "default.dibblerlog"
|
# inputFile = "default.dibblerlog"
|
||||||
database, dateLine = buildDatabaseFromFile(inputFile, inputType, product, user)
|
# database, dateLine = buildDatabaseFromFile(inputFile, inputType, product, user)
|
||||||
else:
|
# else:
|
||||||
database, dateLine = buildDatabaseFromDb(inputType, product, user)
|
# database, dateLine = buildDatabaseFromDb(inputType, product, user)
|
||||||
|
|
||||||
if inputType == 1:
|
# if inputType == 1:
|
||||||
plotUser(database, dateLine, user, 10)
|
# plotUser(database, dateLine, user, 10)
|
||||||
if inputType == 2:
|
# if inputType == 2:
|
||||||
plotProduct(database, dateLine, product, 10)
|
# plotProduct(database, dateLine, product, 10)
|
||||||
if inputType == 3:
|
# if inputType == 3:
|
||||||
plotGlobal(database, dateLine, 10)
|
# plotGlobal(database, dateLine, 10)
|
||||||
if inputType == 4:
|
# if inputType == 4:
|
||||||
alt4menu(database, dateLine, i)
|
# alt4menu(database, dateLine, i)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
# if __name__ == "__main__":
|
||||||
main()
|
# main()
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
[general]
|
|
||||||
quit_allowed = true
|
|
||||||
stop_allowed = false
|
|
||||||
show_tracebacks = true
|
|
||||||
input_encoding = 'utf8'
|
|
||||||
|
|
||||||
[database]
|
|
||||||
; url = postgresql://robertem@127.0.0.1/pvvvv
|
|
||||||
url = sqlite:///test.db
|
|
||||||
|
|
||||||
[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
|
|
||||||
35
example-config.toml
Normal file
35
example-config.toml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[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
|
||||||
46
flake.lock
generated
46
flake.lock
generated
@@ -1,57 +1,25 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1692799911,
|
|
||||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1693145325,
|
"lastModified": 1764950072,
|
||||||
"narHash": "sha256-Gat9xskErH1zOcLjYMhSDBo0JTBZKfGS0xJlIRnj6Rc=",
|
"narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "cddebdb60de376c1bdb7a4e6ee3d98355453fe56",
|
"rev": "f61125a668a320878494449750330ca58b78c557",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"owner": "NixOS",
|
||||||
"type": "indirect"
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
116
flake.nix
116
flake.nix
@@ -1,77 +1,73 @@
|
|||||||
{
|
{
|
||||||
description = "Dibbler samspleisebod";
|
description = "Dibbler samspleisebod";
|
||||||
|
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
outputs = { self, nixpkgs }: let
|
||||||
flake-utils.lib.eachDefaultSystem (system: let
|
inherit (nixpkgs) lib;
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
in {
|
|
||||||
packages = {
|
|
||||||
default = self.packages.${system}.dibbler;
|
|
||||||
dibbler = pkgs.callPackage ./nix/dibbler.nix {
|
|
||||||
python3Packages = pkgs.python311Packages;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
apps = {
|
systems = [
|
||||||
default = self.apps.${system}.dibbler;
|
"x86_64-linux"
|
||||||
dibbler = flake-utils.lib.mkApp {
|
"aarch64-linux"
|
||||||
drv = self.packages.${system}.dibbler;
|
"x86_64-darwin"
|
||||||
};
|
"aarch64-darwin"
|
||||||
};
|
|
||||||
|
|
||||||
devShells = {
|
|
||||||
default = self.devShells.${system}.dibbler;
|
|
||||||
dibbler = pkgs.mkShell {
|
|
||||||
packages = with pkgs; [
|
|
||||||
python311Packages.black
|
|
||||||
ruff
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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} "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
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";
|
||||||
|
});
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
{
|
|
||||||
# Note: using the module requires that you have applied the
|
|
||||||
# overlay first
|
|
||||||
nixosModules.default = import ./nix/module.nix;
|
nixosModules.default = import ./nix/module.nix;
|
||||||
|
|
||||||
images.skrot = self.nixosConfigurations.skrot.config.system.build.sdImage;
|
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; };
|
||||||
|
};
|
||||||
|
|
||||||
nixosConfigurations.skrot = nixpkgs.lib.nixosSystem {
|
overlays = {
|
||||||
system = "aarch64-linux";
|
default = self.overlays.dibbler;
|
||||||
modules = [
|
dibbler = final: prev: {
|
||||||
(nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix")
|
inherit (self.packages.${prev.stdenv.hostPlatform.system}) dibbler;
|
||||||
self.nixosModules.default
|
};
|
||||||
({...}: {
|
};
|
||||||
system.stateVersion = "22.05";
|
|
||||||
|
|
||||||
networking = {
|
devShells = forAllSystems (system: pkgs: {
|
||||||
hostName = "skrot";
|
default = self.devShells.${system}.dibbler;
|
||||||
domain = "pvv.ntnu.no";
|
dibbler = pkgs.callPackage ./nix/shell.nix {
|
||||||
nameservers = [ "129.241.0.200" "129.241.0.201" ];
|
python = pkgs.python313;
|
||||||
defaultGateway = "129.241.210.129";
|
|
||||||
interfaces.eth0 = {
|
|
||||||
useDHCP = false;
|
|
||||||
ipv4.addresses = [{
|
|
||||||
address = "129.241.210.235";
|
|
||||||
prefixLength = 25;
|
|
||||||
}];
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
packages = forAllSystems (system: pkgs: {
|
||||||
|
default = self.packages.${system}.dibbler;
|
||||||
|
dibbler = pkgs.callPackage ./nix/package.nix {
|
||||||
|
python3Packages = pkgs.python313Packages;
|
||||||
|
inherit (self) sourceInfo;
|
||||||
};
|
};
|
||||||
# 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" ];
|
|
||||||
# };
|
|
||||||
})
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
76
mock_data.json
Normal file
76
mock_data.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{ lib
|
|
||||||
, python3Packages
|
|
||||||
, fetchFromGitHub
|
|
||||||
}:
|
|
||||||
python3Packages.buildPythonApplication {
|
|
||||||
pname = "dibbler";
|
|
||||||
version = "unstable-2021-09-07";
|
|
||||||
src = lib.cleanSource ../.;
|
|
||||||
|
|
||||||
format = "pyproject";
|
|
||||||
|
|
||||||
nativeBuildInputs = with python3Packages; [ setuptools ];
|
|
||||||
propagatedBuildInputs = with python3Packages; [
|
|
||||||
brother-ql
|
|
||||||
matplotlib
|
|
||||||
psycopg2
|
|
||||||
python-barcode
|
|
||||||
sqlalchemy
|
|
||||||
];
|
|
||||||
}
|
|
||||||
245
nix/module.nix
245
nix/module.nix
@@ -1,84 +1,215 @@
|
|||||||
{ config, pkgs, lib, ... }: let
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
cfg = config.services.dibbler;
|
cfg = config.services.dibbler;
|
||||||
in {
|
worbleCfg = config.services.worblehat;
|
||||||
|
|
||||||
|
format = pkgs.formats.toml { };
|
||||||
|
in
|
||||||
|
{
|
||||||
options.services.dibbler = {
|
options.services.dibbler = {
|
||||||
|
enable = lib.mkEnableOption "dibbler, the little kiosk computer";
|
||||||
|
|
||||||
package = lib.mkPackageOption pkgs "dibbler" { };
|
package = lib.mkPackageOption pkgs "dibbler" { };
|
||||||
config = lib.mkOption {
|
|
||||||
default = ../conf.py;
|
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 = let
|
config = lib.mkIf cfg.enable (
|
||||||
screen = "${pkgs.screen}/bin/screen";
|
lib.mkMerge [
|
||||||
in {
|
{
|
||||||
boot = {
|
services.dibbler.settings = lib.pipe ../example-config.toml [
|
||||||
consoleLogLevel = 0;
|
builtins.readFile
|
||||||
enableContainers = false;
|
builtins.fromTOML
|
||||||
loader.grub.enable = false;
|
(lib.mapAttrsRecursive (_: lib.mkDefault))
|
||||||
};
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
environment.systemPackages = [
|
||||||
|
cfg.package
|
||||||
|
worbleCfg.package
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.etc."dibbler/dibbler.toml".source = format.generate "dibbler.toml" cfg.settings;
|
||||||
|
|
||||||
users = {
|
users = {
|
||||||
groups.dibbler = { };
|
|
||||||
users.dibbler = {
|
users.dibbler = {
|
||||||
group = "dibbler";
|
group = "dibbler";
|
||||||
extraGroups = [ "lp" ];
|
|
||||||
isNormalUser = true;
|
isNormalUser = true;
|
||||||
shell = ((pkgs.writeShellScriptBin "login-shell" "${screen} -x dibbler") // {shellPath = "/bin/login-shell";});
|
};
|
||||||
|
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 = {
|
||||||
|
extraGroups = [ "lp" ];
|
||||||
|
shell =
|
||||||
|
(pkgs.writeShellScriptBin "login-shell" "${lib.getExe' cfg.screenPackage "screen"} -x dibbler")
|
||||||
|
// {
|
||||||
|
shellPath = "/bin/login-shell";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.screen-daemon = {
|
services.dibbler.settings.general = {
|
||||||
description = "Dibbler service screen";
|
quit_allowed = false;
|
||||||
wantedBy = [ "default.target" ];
|
stop_allowed = 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 = {
|
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";
|
Type = "forking";
|
||||||
RemainAfterExit = false;
|
RemainAfterExit = false;
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
SuccessExitStatus = 1;
|
SuccessExitStatus = 1;
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# https://github.com/NixOS/nixpkgs/issues/84105
|
User = "dibbler";
|
||||||
boot.kernelParams = [
|
Group = "dibbler";
|
||||||
"console=ttyUSB0,9600"
|
|
||||||
"console=tty1"
|
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"
|
||||||
|
|
||||||
|
# Set window name
|
||||||
|
"-t"
|
||||||
|
"dibblerino"
|
||||||
];
|
];
|
||||||
systemd.services."serial-getty@ttyUSB0" = {
|
|
||||||
enable = true;
|
dibblerArgs = lib.cli.toCommandLineShellGNU { } {
|
||||||
wantedBy = [ "getty.target" ]; # to start at boot
|
config = "/etc/dibbler/dibbler.toml";
|
||||||
serviceConfig.Restart = "always"; # restart when session is closed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services = {
|
in
|
||||||
openssh = {
|
"${lib.getExe' cfg.screenPackage "screen"} ${screenArgs} ${lib.getExe cfg.package} ${dibblerArgs} loop";
|
||||||
enable = true;
|
ExecStartPost =
|
||||||
permitRootLogin = "yes";
|
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}"
|
||||||
|
]
|
||||||
|
++ [
|
||||||
|
"${lib.getExe' cfg.screenPackage "screen"} -S dibbler -X screen -t worblehat ${lib.getExe worbleCfg.package}"
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
getty.autologinUser = lib.mkForce "dibbler";
|
services.getty.autologinUser = "dibbler";
|
||||||
udisks2.enable = false;
|
})
|
||||||
};
|
]
|
||||||
|
);
|
||||||
networking.firewall.logRefusedConnections = false;
|
|
||||||
console.keyMap = "no";
|
|
||||||
programs.command-not-found.enable = false;
|
|
||||||
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
|
|
||||||
environment.noXlibs = true;
|
|
||||||
|
|
||||||
documentation = {
|
|
||||||
info.enable = false;
|
|
||||||
man.enable = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
security = {
|
|
||||||
polkit.enable = lib.mkForce false;
|
|
||||||
audit.enable = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
54
nix/nixos-configurations/vm-non-kiosk.nix
Normal file
54
nix/nixos-configurations/vm-non-kiosk.nix
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{ 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;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
29
nix/nixos-configurations/vm.nix
Normal file
29
nix/nixos-configurations/vm.nix
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{ 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;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
49
nix/package.nix
Normal file
49
nix/package.nix
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{ 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";
|
||||||
|
};
|
||||||
|
}
|
||||||
20
nix/shell.nix
Normal file
20
nix/shell.nix
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
mkShell,
|
||||||
|
python,
|
||||||
|
ruff,
|
||||||
|
uv,
|
||||||
|
}:
|
||||||
|
|
||||||
|
mkShell {
|
||||||
|
packages = [
|
||||||
|
ruff
|
||||||
|
uv
|
||||||
|
(python.withPackages (ps: with ps; [
|
||||||
|
# brother-ql
|
||||||
|
# matplotlib
|
||||||
|
psycopg2
|
||||||
|
# python-barcode
|
||||||
|
sqlalchemy
|
||||||
|
]))
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,31 +1,36 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools", "setuptools-scm"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
requires = [
|
||||||
|
"setuptools",
|
||||||
|
"setuptools-scm",
|
||||||
|
]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "dibbler"
|
name = "dibbler"
|
||||||
authors = []
|
dynamic = ["version"]
|
||||||
|
authors = [
|
||||||
|
{ name = "Programvareverkstedet", email = "projects@pvv.ntnu.no" }
|
||||||
|
]
|
||||||
description = "EDB-system for PVV"
|
description = "EDB-system for PVV"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
license = {text = "BSD-3-Clause"}
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"SQLAlchemy >= 2.0, <2.1",
|
"SQLAlchemy >= 2.0, <2.1",
|
||||||
"brother-ql",
|
# "brother-ql",
|
||||||
"matplotlib",
|
# "matplotlib",
|
||||||
"psycopg2 >= 2.8, <2.10",
|
"psycopg2-binary >= 2.8, <2.10",
|
||||||
"python-barcode",
|
# "python-barcode",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
scripts.dibbler = "dibbler.main:main"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["dibbler*"]
|
include = ["dibbler*"]
|
||||||
|
|
||||||
[project.scripts]
|
[tool.setuptools_scm]
|
||||||
dibbler = "dibbler.main:main"
|
version_file = "dibbler/_version.py"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
@@ -33,3 +38,34 @@ line-length = 100
|
|||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
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
|
||||||
|
|||||||
167
uv.lock
generated
Normal file
167
uv.lock
generated
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
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.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/d5/c339b3b4bc8198b7caa4f2bd9fd685ac9f29795816d8db112da3d04175bb/greenlet-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71", size = 301164, upload-time = "2025-12-04T14:42:51.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2-binary"
|
||||||
|
version = "2.9.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlalchemy"
|
||||||
|
version = "2.0.45"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user