Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91f924a75a | |||
| e1605aab29 | |||
| 1164d492a3 |
@@ -1 +1,2 @@
|
|||||||
use flake
|
# devenv needs to know the path to the current working directory to create and manage mutable state
|
||||||
|
use flake . --no-pure-eval
|
||||||
|
|||||||
+5
-6
@@ -1,13 +1,12 @@
|
|||||||
result
|
result
|
||||||
result-*
|
result-*
|
||||||
**/__pycache__
|
.venv
|
||||||
dibbler.egg-info
|
.direnv
|
||||||
|
.devenv
|
||||||
|
|
||||||
dist
|
dist
|
||||||
|
|
||||||
|
config.ini
|
||||||
test.db
|
test.db
|
||||||
|
|
||||||
.ruff_cache
|
.ruff_cache
|
||||||
|
|
||||||
*.qcow2
|
|
||||||
|
|
||||||
dibbler/_version.py
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
Robert Eric Maikher <robertem@pvv.ntnu.no> Robert Maikher <robertem342@gmail.com>
|
|
||||||
Robert Eric Maikher <robertem@pvv.ntnu.no> robertem <3456661+RandomGamer342@users.noreply.github.com>
|
|
||||||
|
|
||||||
Øystein Kristoffer Tveit <oysteikt@pvv.ntnu.no> h7x4 <h7x4@nani.wtf>
|
|
||||||
Øystein Kristoffer Tveit <oysteikt@pvv.ntnu.no> Oystein Kristoffer Tveit <oysteikt@pvv.ntnu.no>
|
|
||||||
Øystein Kristoffer Tveit <oysteikt@pvv.ntnu.no> Øystein Tveit <h7x4@nani.wtf>
|
|
||||||
@@ -2,55 +2,47 @@
|
|||||||
|
|
||||||
EDB-system for PVVVV
|
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
|
||||||
|
|
||||||
> [!NOTE]
|
### Hvordan kjøre
|
||||||
> 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:
|
nix run github:Programvareverkstedet/dibbler
|
||||||
|
|
||||||
```console
|
### Hvordan utvikle?
|
||||||
nix run .#vm
|
|
||||||
|
|
||||||
# Eller hvis du trenger tilgang til terminalen i VM-en også:
|
python -m venv .venv
|
||||||
nix run .#vm-non-kiosk
|
source .venv/activate
|
||||||
```
|
pip install -e .
|
||||||
|
cp example-config.ini config.ini
|
||||||
|
dibbler -c config.ini create-db
|
||||||
|
dibbler -c config.ini loop
|
||||||
|
|
||||||
Du kan også bygge pakken manuelt, eller kjøre den direkte:
|
eller hvis du tolererer nix og postgres:
|
||||||
|
|
||||||
```console
|
direnv allow # eller bare `nix develop`
|
||||||
nix build .#dibbler
|
devenv up
|
||||||
|
dibbler create-db
|
||||||
|
dibbler loop
|
||||||
|
|
||||||
nix run .# -- --config example-config.toml create-db
|
### Bygge image
|
||||||
nix run .# -- --config example-config.toml seed-data
|
|
||||||
nix run .# -- --config example-config.toml loop
|
|
||||||
```
|
|
||||||
|
|
||||||
## Produksjonssetting
|
For å bygge et image trenger du en builder som takler å bygge for arkitekturen du skal lage et image for.
|
||||||
|
|
||||||
Se https://wiki.pvv.ntnu.no/wiki/Drift/Dibbler
|
_(Eller be til gudene om at cross compile funker)_
|
||||||
|
|
||||||
|
Flaket exposer en modul som autologger inn med en bruker som automatisk kjører dibbler, og setter opp et minimalistisk miljø.
|
||||||
|
|
||||||
|
Før du bygger imaget burde du lage en `config.ini` fil lokalt som inneholder instillingene dine. **NB: Denne kommer til å ligge i nix storen.**
|
||||||
|
|
||||||
|
Du kan også endre hvilken `config.ini` som blir brukt direkte i pakken eller i modulen.
|
||||||
|
|
||||||
|
Se eksempelet for hvordan skrot er satt opp i `flake.nix`
|
||||||
|
|
||||||
|
### Bygge image for skrot
|
||||||
|
|
||||||
|
Skrot har et system image definert i `flake.nix`:
|
||||||
|
|
||||||
|
1. lag `config.ini` (`cp {example-,}config.ini`)
|
||||||
|
2. `nix build .#images.skrot`
|
||||||
|
3. ???
|
||||||
|
4. non-profit!
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{ pkgs ? import <nixos-unstable> { } }:
|
||||||
|
{
|
||||||
|
dibbler = pkgs.callPackage ./nix/dibbler.nix { };
|
||||||
|
}
|
||||||
+4
-54
@@ -1,56 +1,6 @@
|
|||||||
import os
|
# This module is supposed to act as a singleton and be filled
|
||||||
import sys
|
# with config variables by cli.py
|
||||||
import tomllib
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from dibbler.lib.helpers import file_is_submissive_and_readable
|
import configparser
|
||||||
|
|
||||||
DEFAULT_CONFIG_PATH = Path("/etc/dibbler/dibbler.toml")
|
config = configparser.ConfigParser()
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from dibbler.conf import config
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
os.environ.get("DIBBLER_DATABASE_URL")
|
||||||
|
or config.get("database", "url")
|
||||||
|
)
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
@@ -1,71 +1,70 @@
|
|||||||
# import os
|
import os
|
||||||
|
|
||||||
# from PIL import ImageFont
|
from PIL import ImageFont
|
||||||
# from barcode.writer import ImageWriter, mm2px
|
from barcode.writer import ImageWriter, mm2px
|
||||||
# from brother_ql.labels import ALL_LABELS
|
from brother_ql.devicedependent import label_type_specs
|
||||||
|
|
||||||
|
|
||||||
# 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__()
|
||||||
# label = next([l for l in ALL_LABELS if l.identifier == typ])
|
assert typ in label_type_specs
|
||||||
# assert label is not None
|
self.rot = rot
|
||||||
# self.rot = rot
|
if self.rot:
|
||||||
# if self.rot:
|
self._h, self._w = label_type_specs[typ]["dots_printable"]
|
||||||
# self._h, self._w = label.dots_printable
|
if self._w == 0 or self._w > max_height:
|
||||||
# if self._w == 0 or self._w > max_height:
|
self._w = min(max_height, self._h / 2)
|
||||||
# self._w = min(max_height, self._h / 2)
|
else:
|
||||||
# else:
|
self._w, self._h = label_type_specs[typ]["dots_printable"]
|
||||||
# self._w, self._h = label.dots_printable
|
if self._h == 0 or self._h > max_height:
|
||||||
# if self._h == 0 or self._h > max_height:
|
self._h = min(max_height, self._w / 2)
|
||||||
# self._h = min(max_height, self._w / 2)
|
self._xo = 0.0
|
||||||
# self._xo = 0.0
|
self._yo = 0.0
|
||||||
# self._yo = 0.0
|
self._title = text
|
||||||
# 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
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from sqlalchemy import Engine, create_engine, inspect, select
|
|
||||||
from sqlalchemy.exc import DBAPIError, OperationalError
|
|
||||||
from sqlalchemy.orm import RelationshipProperty
|
|
||||||
from sqlalchemy.orm.clsregistry import _ModuleMarker
|
|
||||||
|
|
||||||
from dibbler.lib.helpers import file_is_submissive_and_readable
|
|
||||||
from dibbler.models import Base
|
|
||||||
|
|
||||||
|
|
||||||
def check_db_health(engine: Engine, verify_table_existence: bool = False) -> None:
|
|
||||||
dialect_name = getattr(engine.dialect, "name", "").lower()
|
|
||||||
|
|
||||||
if "postgres" in dialect_name:
|
|
||||||
check_postgres_ping(engine)
|
|
||||||
|
|
||||||
elif dialect_name == "sqlite":
|
|
||||||
check_sqlite_file(engine)
|
|
||||||
|
|
||||||
if verify_table_existence:
|
|
||||||
verify_tables_and_columns(engine)
|
|
||||||
|
|
||||||
|
|
||||||
def check_postgres_ping(engine: Engine) -> None:
|
|
||||||
try:
|
|
||||||
with engine.connect() as conn:
|
|
||||||
result = conn.execute(select(1))
|
|
||||||
scalar = result.scalar()
|
|
||||||
if scalar != 1 and scalar is not None:
|
|
||||||
print(
|
|
||||||
"Unexpected response from Postgres when running 'SELECT 1'",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
except (OperationalError, DBAPIError) as exc:
|
|
||||||
print(f"Failed to connect to Postgres database: {exc}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def check_sqlite_file(engine: Engine) -> None:
|
|
||||||
db_path = engine.url.database
|
|
||||||
|
|
||||||
# Don't verify in-memory databases or empty paths
|
|
||||||
if db_path in (None, "", ":memory:"):
|
|
||||||
return
|
|
||||||
|
|
||||||
db_path = db_path.removeprefix("file:").removeprefix("sqlite:")
|
|
||||||
|
|
||||||
# Strip query parameters
|
|
||||||
if "?" in db_path:
|
|
||||||
db_path = db_path.split("?", 1)[0]
|
|
||||||
|
|
||||||
path = Path(db_path)
|
|
||||||
|
|
||||||
if not path.exists():
|
|
||||||
print(f"SQLite database file does not exist: {path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not path.is_file():
|
|
||||||
print(f"SQLite database path is not a file: {path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not file_is_submissive_and_readable(path):
|
|
||||||
print(f"SQLite database file is not submissive and readable: {path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def verify_tables_and_columns(engine: Engine) -> None:
|
|
||||||
iengine = inspect(engine)
|
|
||||||
errors = False
|
|
||||||
tables = iengine.get_table_names()
|
|
||||||
views = iengine.get_view_names()
|
|
||||||
tables.extend(views)
|
|
||||||
|
|
||||||
for _name, klass in Base.registry._class_registry.items():
|
|
||||||
if isinstance(klass, _ModuleMarker):
|
|
||||||
continue
|
|
||||||
|
|
||||||
table = klass.__tablename__
|
|
||||||
if table in tables:
|
|
||||||
columns = [c["name"] for c in iengine.get_columns(table)]
|
|
||||||
mapper = inspect(klass)
|
|
||||||
|
|
||||||
for column_prop in mapper.attrs:
|
|
||||||
if isinstance(column_prop, RelationshipProperty):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
for column in column_prop.columns:
|
|
||||||
if not column.key in columns:
|
|
||||||
print(
|
|
||||||
f"Model '{klass}' declares column '{column.key}' which does not exist in database {engine}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
errors = True
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"Model '{klass}' declares table '{table}' which does not exist in database {engine}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
errors = True
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
print("Have you remembered to run `dibbler create-db?", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
+25
-60
@@ -1,69 +1,51 @@
|
|||||||
import os
|
|
||||||
import pwd
|
import pwd
|
||||||
import signal
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from collections.abc import Callable
|
import os
|
||||||
from pathlib import Path
|
import signal
|
||||||
from typing import Any, Literal
|
|
||||||
|
|
||||||
from sqlalchemy import and_, not_, or_
|
from sqlalchemy import or_, and_
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from ..models import Product, User
|
from ..models import User, Product
|
||||||
|
|
||||||
|
|
||||||
def search_user(
|
def search_user(string, session, ignorethisflag=None):
|
||||||
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 = (
|
||||||
sql_session.query(User)
|
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
|
||||||
return (
|
user_list = (
|
||||||
sql_session.query(User)
|
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(
|
def search_product(string, session, find_hidden_products=True):
|
||||||
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 = (
|
||||||
sql_session.query(Product)
|
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 = (
|
||||||
sql_session.query(Product)
|
session.query(Product)
|
||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
Product.bar_code == string,
|
Product.bar_code == string,
|
||||||
and_(
|
and_(Product.name == string, Product.hidden is False),
|
||||||
Product.name == string,
|
)
|
||||||
not_(Product.hidden),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
@@ -71,33 +53,30 @@ def search_product(
|
|||||||
return exact_match
|
return exact_match
|
||||||
if find_hidden_products:
|
if find_hidden_products:
|
||||||
product_list = (
|
product_list = (
|
||||||
sql_session.query(Product)
|
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 = (
|
||||||
sql_session.query(Product)
|
session.query(Product)
|
||||||
.filter(
|
.filter(
|
||||||
or_(
|
or_(
|
||||||
Product.bar_code.ilike(f"%{string}%"),
|
Product.bar_code.ilike(f"%{string}%"),
|
||||||
and_(
|
and_(Product.name.ilike(f"%{string}%"), Product.hidden is False),
|
||||||
Product.name.ilike(f"%{string}%"),
|
)
|
||||||
not_(Product.hidden),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return product_list
|
return product_list
|
||||||
|
|
||||||
|
|
||||||
def system_user_exists(username: str) -> bool:
|
def system_user_exists(username):
|
||||||
try:
|
try:
|
||||||
pwd.getpwnam(username)
|
pwd.getpwnam(username)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -108,7 +87,7 @@ def system_user_exists(username: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def guess_data_type(string: str) -> Literal["card", "rfid", "bar_code", "username"] | None:
|
def guess_data_type(string):
|
||||||
if string.startswith("ntnu") and string[4:].isdigit():
|
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:
|
||||||
@@ -122,11 +101,7 @@ def guess_data_type(string: str) -> Literal["card", "rfid", "bar_code", "usernam
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def argmax(
|
def argmax(d, all=False, value=None):
|
||||||
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
|
||||||
@@ -136,12 +111,12 @@ def argmax(
|
|||||||
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: str) -> None:
|
def less(string):
|
||||||
"""
|
"""
|
||||||
Run less with string as input; wait until it finishes.
|
Run less with string as input; wait until it finishes.
|
||||||
"""
|
"""
|
||||||
@@ -153,13 +128,3 @@ def less(string: str) -> None:
|
|||||||
proc = subprocess.Popen("less", env=env, encoding="utf-8", stdin=subprocess.PIPE)
|
proc = 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,95 +1,96 @@
|
|||||||
# import barcode
|
import os
|
||||||
# from brother_ql.brother_ql_create import create_label
|
import datetime
|
||||||
# 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
|
|
||||||
|
|
||||||
# from .barcode_helpers import BrotherLabelWriter
|
import barcode
|
||||||
|
from brother_ql import BrotherQLRaster, create_label
|
||||||
|
from brother_ql.backends import backend_factory
|
||||||
|
from brother_ql.devicedependent import label_type_specs
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
from .barcode_helpers import BrotherLabelWriter
|
||||||
|
|
||||||
|
|
||||||
# def print_name_label(
|
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",
|
||||||
# ):
|
):
|
||||||
# label = next([l for l in ALL_LABELS if l.identifier == label_type])
|
if not rotate:
|
||||||
# if not rotate:
|
width, height = label_type_specs[label_type]["dots_printable"]
|
||||||
# width, height = label.dots_printable
|
else:
|
||||||
# else:
|
height, width = label_type_specs[label_type]["dots_printable"]
|
||||||
# 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,20 +3,18 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from ..models import Transaction
|
|
||||||
from .helpers import *
|
from .helpers import *
|
||||||
|
from ..models import Transaction
|
||||||
|
from ..db import Session
|
||||||
|
|
||||||
|
|
||||||
def getUser(sql_session: Session) -> str:
|
def getUser():
|
||||||
assert sql_session is not None
|
|
||||||
while 1:
|
while 1:
|
||||||
string = input("user? ")
|
string = input("user? ")
|
||||||
user = search_user(string, sql_session)
|
session = Session()
|
||||||
sql_session.close()
|
user = search_user(string, session)
|
||||||
|
session.close()
|
||||||
if not isinstance(user, list):
|
if not isinstance(user, list):
|
||||||
return user.name
|
return user.name
|
||||||
i = 0
|
i = 0
|
||||||
@@ -39,11 +37,12 @@ def getUser(sql_session: Session) -> str:
|
|||||||
return user[n].name
|
return user[n].name
|
||||||
|
|
||||||
|
|
||||||
def getProduct(sql_session: Session) -> str:
|
def getProduct():
|
||||||
assert sql_session is not None
|
|
||||||
while 1:
|
while 1:
|
||||||
string = input("product? ")
|
string = input("product? ")
|
||||||
product = search_product(string, sql_session)
|
session = 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
|
||||||
@@ -77,8 +76,12 @@ class Database:
|
|||||||
personDatoVerdi = defaultdict(list) # dict->array
|
personDatoVerdi = defaultdict(list) # dict->array
|
||||||
personUkedagVerdi = defaultdict(list)
|
personUkedagVerdi = defaultdict(list)
|
||||||
# for global
|
# for global
|
||||||
personPosTransactions = {} # personPosTransactions[trygvrad] == 100 #trygvrad har lagt 100kr i boksen
|
personPosTransactions = (
|
||||||
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
|
||||||
@@ -90,7 +93,7 @@ class Database:
|
|||||||
|
|
||||||
|
|
||||||
class InputLine:
|
class InputLine:
|
||||||
def __init__(self, u, p, t) -> None:
|
def __init__(self, u, p, t):
|
||||||
self.inputUser = u
|
self.inputUser = u
|
||||||
self.inputProduct = p
|
self.inputProduct = p
|
||||||
self.inputType = t
|
self.inputType = t
|
||||||
@@ -123,17 +126,17 @@ def getInputType():
|
|||||||
return int(inp)
|
return int(inp)
|
||||||
|
|
||||||
|
|
||||||
def getProducts(products: str) -> list[tuple[str]]:
|
def getProducts(products):
|
||||||
product = []
|
product = []
|
||||||
split_products = products.partition("¤")
|
products = products.partition("¤")
|
||||||
product.append(products[0])
|
product.append(products[0])
|
||||||
while products[1] == "¤":
|
while products[1] == "¤":
|
||||||
split_products = split_products[2].partition("¤")
|
products = products[2].partition("¤")
|
||||||
product.append(products[0])
|
product.append(products[0])
|
||||||
return product
|
return product
|
||||||
|
|
||||||
|
|
||||||
def getDateFile(date: str, inp: str) -> datetime.date:
|
def getDateFile(date, inp):
|
||||||
try:
|
try:
|
||||||
year = inp.partition("-")
|
year = inp.partition("-")
|
||||||
month = year[2].partition("-")
|
month = year[2].partition("-")
|
||||||
@@ -177,7 +180,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 (inputLine.product != "") and (
|
if (not 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] = (
|
||||||
@@ -191,7 +194,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 inputLine.product != "":
|
if not 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
|
||||||
)
|
)
|
||||||
@@ -215,7 +218,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 inputLine.inputType != 1:
|
elif not (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
|
||||||
)
|
)
|
||||||
@@ -226,7 +229,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 inputLine.product != "":
|
if not (inputLine.product == ""):
|
||||||
database.globalPersonAntall[inputLine.user] = (
|
database.globalPersonAntall[inputLine.user] = (
|
||||||
database.globalPersonAntall.setdefault(inputLine.user, 0) + 1
|
database.globalPersonAntall.setdefault(inputLine.user, 0) + 1
|
||||||
)
|
)
|
||||||
@@ -239,12 +242,12 @@ def addLineToDatabase(database, inputLine):
|
|||||||
return database
|
return database
|
||||||
|
|
||||||
|
|
||||||
def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session):
|
def buildDatabaseFromDb(inputType, inputProduct, inputUser):
|
||||||
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...")
|
||||||
transaction_list = sql_session.query(Transaction).all()
|
session = Session()
|
||||||
|
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)
|
||||||
@@ -274,9 +277,9 @@ def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session
|
|||||||
inputLine.price = 0
|
inputLine.price = 0
|
||||||
|
|
||||||
print("saving as default.dibblerlog...", end=" ")
|
print("saving as default.dibblerlog...", end=" ")
|
||||||
f = Path.open("default.dibblerlog", "w")
|
f = open("default.dibblerlog", "w")
|
||||||
line_format = "%s|%s|%s|%s|%s|%s\n"
|
line_format = "%s|%s|%s|%s|%s|%s\n"
|
||||||
transaction_list = sql_session.query(Transaction).all()
|
transaction_list = session.query(Transaction).all()
|
||||||
for transaction in transaction_list:
|
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])
|
||||||
@@ -291,7 +294,8 @@ def buildDatabaseFromDb(inputType, inputProduct, inputUser, sql_session: Session
|
|||||||
transaction.description,
|
transaction.description,
|
||||||
)
|
)
|
||||||
f.write(line.encode("utf8"))
|
f.write(line.encode("utf8"))
|
||||||
f.close()
|
session.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):
|
||||||
@@ -311,7 +315,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 = Path.open(inputFile)
|
f = open(inputFile)
|
||||||
try:
|
try:
|
||||||
fileLines = f.readlines()
|
fileLines = f.readlines()
|
||||||
finally:
|
finally:
|
||||||
@@ -329,7 +333,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 linje[0] != "#" and linje != "\n":
|
if not (linje[0] == "#") and not (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(" ")
|
||||||
@@ -359,7 +363,7 @@ def buildDatabaseFromFile(inputFile, inputType, inputProduct, inputUser):
|
|||||||
return database, dateLine
|
return database, dateLine
|
||||||
|
|
||||||
|
|
||||||
def printTopDict(dictionary: dict[str, Any], n: int, k: bool) -> None:
|
def printTopDict(dictionary, n, k):
|
||||||
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])
|
||||||
@@ -369,7 +373,7 @@ def printTopDict(dictionary: dict[str, Any], n: int, k: bool) -> None:
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def printTopDict2(dictionary, dictionary2, n) -> None:
|
def printTopDict2(dictionary, dictionary2, n):
|
||||||
print("")
|
print("")
|
||||||
print("product : price[kr] ( number )")
|
print("product : price[kr] ( number )")
|
||||||
i = 0
|
i = 0
|
||||||
@@ -381,7 +385,7 @@ def printTopDict2(dictionary, dictionary2, n) -> None:
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def printWeekdays(week, days) -> None:
|
def printWeekdays(week, days):
|
||||||
if week == [] or days == 0:
|
if week == [] or days == 0:
|
||||||
return
|
return
|
||||||
print(
|
print(
|
||||||
@@ -404,10 +408,10 @@ def printWeekdays(week, days) -> None:
|
|||||||
print("")
|
print("")
|
||||||
|
|
||||||
|
|
||||||
def printBalance(database, user) -> None:
|
def printBalance(database, user):
|
||||||
forbruk = 0
|
forbruk = 0
|
||||||
if user in database.personVareVerdi:
|
if user in database.personVareVerdi:
|
||||||
forbruk = sum(database.personVareVerdi[user].values())
|
forbruk = sum([i for i in list(database.personVareVerdi[user].values())])
|
||||||
print("totalt kjøpt for: ", forbruk, end=" ")
|
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=" ")
|
||||||
@@ -419,14 +423,14 @@ def printBalance(database, user) -> None:
|
|||||||
print("")
|
print("")
|
||||||
|
|
||||||
|
|
||||||
def printUser(database, dateLine, user, n) -> None:
|
def printUser(database, dateLine, user, n):
|
||||||
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) -> None:
|
def printProduct(database, dateLine, product, n):
|
||||||
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))
|
||||||
@@ -440,7 +444,7 @@ def printProduct(database, dateLine, product, n) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def printGlobal(database, dateLine, n) -> None:
|
def printGlobal(database, dateLine, n):
|
||||||
print("\nmest lagt til: ")
|
print("\nmest lagt til: ")
|
||||||
printTopDict(database.personNegTransactions, n, 0)
|
printTopDict(database.personNegTransactions, n, 0)
|
||||||
print("\nmest tatt fra:")
|
print("\nmest tatt fra:")
|
||||||
@@ -454,9 +458,9 @@ def printGlobal(database, dateLine, n) -> None:
|
|||||||
"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(database.personNegTransactions.values()),
|
-sum([i for i in list(database.personNegTransactions.values())]),
|
||||||
"og tatt fra",
|
"og tatt fra",
|
||||||
sum(database.personPosTransactions.values()),
|
sum([i for i in list(database.personPosTransactions.values())]),
|
||||||
end=" ",
|
end=" ",
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
@@ -466,24 +470,23 @@ def printGlobal(database, dateLine, n) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def alt4menuTextOnly(database, dateLine, sql_session: Session) -> None:
|
def alt4menuTextOnly(database, dateLine):
|
||||||
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
|
||||||
if inp == "1":
|
elif inp == "1":
|
||||||
try:
|
try:
|
||||||
printUser(database, dateLine, getUser(sql_session), n)
|
printUser(database, dateLine, getUser(), 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(sql_session), n)
|
printProduct(database, dateLine, getProduct(), 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":
|
||||||
@@ -495,16 +498,15 @@ def alt4menuTextOnly(database, dateLine, sql_session: Session) -> None:
|
|||||||
n = int(input("set number to show "))
|
n = int(input("set number to show "))
|
||||||
|
|
||||||
|
|
||||||
def statisticsTextOnly(sql_session: Session) -> None:
|
def statisticsTextOnly():
|
||||||
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, sql_session)
|
database, dateLine = buildDatabaseFromDb(inputType, product, user)
|
||||||
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 inp != "q":
|
if not inp == "q":
|
||||||
alt4menuTextOnly(database, dateLine, sql_session)
|
alt4menuTextOnly(database, dateLine)
|
||||||
|
|||||||
+12
-53
@@ -1,12 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
import os
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from dibbler.conf import config
|
||||||
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()
|
||||||
|
|
||||||
@@ -14,78 +10,41 @@ parser.add_argument(
|
|||||||
"-c",
|
"-c",
|
||||||
"--config",
|
"--config",
|
||||||
help="Path to the config file",
|
help="Path to the config file",
|
||||||
type=Path,
|
type=str,
|
||||||
metavar="FILE",
|
|
||||||
required=False,
|
required=False,
|
||||||
)
|
default=os.environ.get("DIBBLER_CONFIG_FILE", None)
|
||||||
|
|
||||||
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() -> None:
|
def main():
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if args.config is None:
|
||||||
if args.version:
|
print("ERROR: no config was provided", file=sys.stderr)
|
||||||
from ._version import commit_id, version
|
config.read(args.config)
|
||||||
|
|
||||||
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(sql_session)
|
loop.main()
|
||||||
|
|
||||||
elif args.subcommand == "create-db":
|
elif args.subcommand == "create-db":
|
||||||
import dibbler.subcommands.makedb as makedb
|
import dibbler.subcommands.makedb as makedb
|
||||||
|
|
||||||
makedb.main(engine)
|
makedb.main()
|
||||||
|
|
||||||
elif args.subcommand == "slabbedasker":
|
elif args.subcommand == "slabbedasker":
|
||||||
import dibbler.subcommands.slabbedasker as slabbedasker
|
import dibbler.subcommands.slabbedasker as slabbedasker
|
||||||
|
|
||||||
slabbedasker.main(sql_session)
|
slabbedasker.main()
|
||||||
|
|
||||||
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 (
|
||||||
AddProductMenu,
|
|
||||||
AddUserMenu,
|
AddUserMenu,
|
||||||
|
EditUserMenu,
|
||||||
|
AddProductMenu,
|
||||||
|
EditProductMenu,
|
||||||
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 (
|
||||||
AdjustCreditMenu,
|
|
||||||
ProductListMenu,
|
|
||||||
ProductSearchMenu,
|
ProductSearchMenu,
|
||||||
ShowUserMenu,
|
|
||||||
TransferMenu,
|
TransferMenu,
|
||||||
|
AdjustCreditMenu,
|
||||||
UserListMenu,
|
UserListMenu,
|
||||||
|
ShowUserMenu,
|
||||||
|
ProductListMenu,
|
||||||
)
|
)
|
||||||
from .printermenu import PrintLabelMenu
|
from .printermenu import PrintLabelMenu
|
||||||
from .stats import (
|
from .stats import (
|
||||||
BalanceMenu,
|
|
||||||
LoggedStatisticsMenu,
|
|
||||||
ProductPopularityMenu,
|
ProductPopularityMenu,
|
||||||
ProductRevenueMenu,
|
ProductRevenueMenu,
|
||||||
|
BalanceMenu,
|
||||||
|
LoggedStatisticsMenu,
|
||||||
)
|
)
|
||||||
|
|||||||
+12
-21
@@ -1,7 +1,6 @@
|
|||||||
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,
|
||||||
@@ -10,13 +9,12 @@ 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, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Add stock and adjust credit", sql_session)
|
Menu.__init__(self, "Add stock and adjust credit", uses_db=True)
|
||||||
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"""
|
||||||
@@ -25,7 +23,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, **_kwargs) -> bool | None:
|
def _execute(self):
|
||||||
questions = {
|
questions = {
|
||||||
(
|
(
|
||||||
False,
|
False,
|
||||||
@@ -88,10 +86,10 @@ much money you're due in credits for the purchase when prompted.\n"""
|
|||||||
|
|
||||||
self.perform_transaction()
|
self.perform_transaction()
|
||||||
|
|
||||||
def complete_input(self) -> bool:
|
def complete_input(self):
|
||||||
return self.users is not None and len(self.products) > 0 and self.price > 0
|
return bool(self.users) and len(self.products) and self.price
|
||||||
|
|
||||||
def print_info(self) -> None:
|
def print_info(self):
|
||||||
width = 6 + Product.name_length
|
width = 6 + Product.name_length
|
||||||
print()
|
print()
|
||||||
print(width * "-")
|
print(width * "-")
|
||||||
@@ -111,12 +109,7 @@ much money you're due in credits for the purchase when prompted.\n"""
|
|||||||
print(f"{self.products[product][0]}".rjust(width - len(product.name)))
|
print(f"{self.products[product][0]}".rjust(width - len(product.name)))
|
||||||
print(width * "-")
|
print(width * "-")
|
||||||
|
|
||||||
def add_thing_to_pending(
|
def add_thing_to_pending(self, thing, amount, price):
|
||||||
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()):
|
||||||
@@ -126,7 +119,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) -> None:
|
def perform_transaction(self):
|
||||||
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)
|
||||||
@@ -139,11 +132,10 @@ 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],
|
self.products[product][0], product.stock + self.products[product][0]
|
||||||
product.stock + self.products[product][0],
|
|
||||||
)
|
)
|
||||||
product.hidden = False
|
product.hidden = False
|
||||||
print(
|
print(
|
||||||
@@ -159,14 +151,13 @@ much money you're due in credits for the purchase when prompted.\n"""
|
|||||||
PurchaseEntry(purchase, product, -self.products[product][0])
|
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.sql_session.add(purchase)
|
self.session.add(purchase)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.sql_session.commit()
|
self.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}")
|
||||||
|
|||||||
+46
-69
@@ -1,8 +1,4 @@
|
|||||||
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 (
|
||||||
@@ -17,11 +13,10 @@ from .helpermenus import Menu
|
|||||||
|
|
||||||
|
|
||||||
class BuyMenu(Menu):
|
class BuyMenu(Menu):
|
||||||
superfast_mode: bool
|
def __init__(self, session=None):
|
||||||
purchase: Purchase
|
Menu.__init__(self, "Buy", uses_db=True)
|
||||||
|
if session:
|
||||||
def __init__(self, sql_session: Session) -> None:
|
self.session = session
|
||||||
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.
|
||||||
@@ -33,7 +28,7 @@ addition, and you can type 'what' at any time to redisplay it.
|
|||||||
When finished, write an empty line to confirm the purchase.\n"""
|
When finished, write an empty line to confirm the purchase.\n"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def credit_check(user: User) -> bool:
|
def credit_check(user):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param user:
|
:param user:
|
||||||
@@ -42,32 +37,28 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
"""
|
"""
|
||||||
assert isinstance(user, User)
|
assert isinstance(user, User)
|
||||||
|
|
||||||
return user.credit > config["limits"]["low_credit_warning_limit"]
|
return user.credit > config.getint("limits", "low_credit_warning_limit")
|
||||||
|
|
||||||
def low_credit_warning(
|
def low_credit_warning(self, user, timeout=False):
|
||||||
self,
|
|
||||||
user: User,
|
|
||||||
timeout: bool = False,
|
|
||||||
) -> bool:
|
|
||||||
assert isinstance(user, User)
|
assert isinstance(user, User)
|
||||||
|
|
||||||
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(
|
print(
|
||||||
f"USER {user.name} HAS LOWER CREDIT THAN {config['limits']['low_credit_warning_limit']:d}.",
|
f"USER {user.name} HAS LOWER CREDIT THAN {config.getint('limits', 'low_credit_warning_limit'):d}."
|
||||||
)
|
)
|
||||||
print("THIS PURCHASE WILL CHARGE YOUR CREDIT TWICE AS MUCH.")
|
print("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.")
|
||||||
@@ -77,13 +68,10 @@ 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)
|
||||||
return self.confirm(prompt=">", default=True)
|
else:
|
||||||
|
return self.confirm(prompt=">", default=True)
|
||||||
|
|
||||||
def add_thing_to_purchase(
|
def add_thing_to_purchase(self, thing, amount=1):
|
||||||
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("---------------------------------------------")
|
||||||
@@ -92,10 +80,7 @@ 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(
|
if self.low_credit_warning(user=thing, timeout=self.superfast_mode):
|
||||||
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
|
||||||
@@ -110,11 +95,7 @@ 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(
|
def _execute(self, initial_contents=None):
|
||||||
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
|
||||||
@@ -126,7 +107,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: Any) -> bool:
|
def is_product(candidate):
|
||||||
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)):
|
||||||
@@ -148,7 +129,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):
|
||||||
@@ -166,16 +147,16 @@ 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?",
|
"Not enough information entered. Abort purchase?", default=True
|
||||||
default=True,
|
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
# once we get something in the
|
else:
|
||||||
# purchase, we want to protect the
|
# once we get something in the
|
||||||
# user from accidentally killing it
|
# purchase, we want to protect the
|
||||||
self.exit_confirm_msg = "Abort purchase?"
|
# user from accidentally killing it
|
||||||
|
self.exit_confirm_msg = "Abort purchase?"
|
||||||
|
|
||||||
# Add the thing to our purchase object:
|
# Add the thing to our purchase object:
|
||||||
if not self.add_thing_to_purchase(thing, amount=num):
|
if not self.add_thing_to_purchase(thing, amount=num):
|
||||||
@@ -186,11 +167,10 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
break
|
break
|
||||||
|
|
||||||
self.purchase.perform_purchase()
|
self.purchase.perform_purchase()
|
||||||
self.sql_session.add(self.purchase)
|
self.session.add(self.purchase)
|
||||||
try:
|
try:
|
||||||
self.sql_session.commit()
|
self.session.commit()
|
||||||
except SQLAlchemyError as e:
|
except sqlalchemy.exc.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.")
|
||||||
@@ -198,9 +178,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["limits"]["low_credit_warning_limit"]:
|
if t.user.credit < config.getint("limits", "low_credit_warning_limit"):
|
||||||
print(
|
print(
|
||||||
f"USER {t.user.name} HAS LOWER CREDIT THAN {config['limits']['low_credit_warning_limit']:d},",
|
f'USER {t.user.name} HAS LOWER CREDIT THAN {config.getint("limits", "low_credit_warning_limit"):d},',
|
||||||
"AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.",
|
"AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -209,10 +189,10 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
print("")
|
print("")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def complete_input(self) -> bool:
|
def complete_input(self):
|
||||||
return self.purchase.is_complete()
|
return self.purchase.is_complete()
|
||||||
|
|
||||||
def format_purchase(self) -> str | None:
|
def format_purchase(self):
|
||||||
self.purchase.set_price()
|
self.purchase.set_price()
|
||||||
transactions = self.purchase.transactions
|
transactions = self.purchase.transactions
|
||||||
entries = self.purchase.entries
|
entries = self.purchase.entries
|
||||||
@@ -224,10 +204,7 @@ 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:
|
||||||
@@ -235,7 +212,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"
|
||||||
@@ -251,7 +228,7 @@ When finished, write an empty line to confirm the purchase.\n"""
|
|||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def print_purchase(self) -> None:
|
def print_purchase(self):
|
||||||
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)
|
||||||
|
|||||||
+39
-68
@@ -1,9 +1,6 @@
|
|||||||
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__ = [
|
||||||
@@ -17,48 +14,32 @@ __all__ = [
|
|||||||
|
|
||||||
|
|
||||||
class AddUserMenu(Menu):
|
class AddUserMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Add user", sql_session)
|
Menu.__init__(self, "Add user", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
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),
|
||||||
)
|
)
|
||||||
assert username is not None
|
cardnum = self.input_str("Card number (optional)", regex=User.card_re, length_range=(0, 10))
|
||||||
|
cardnum = cardnum.lower()
|
||||||
cardnum = self.input_str(
|
rfid = self.input_str("RFID (optional)", regex=User.rfid_re, length_range=(0, 10))
|
||||||
"Card number (optional)",
|
|
||||||
regex=User.card_re,
|
|
||||||
length_range=(0, 10),
|
|
||||||
empty_string_is_none=True,
|
|
||||||
)
|
|
||||||
if cardnum is not None:
|
|
||||||
cardnum = cardnum.lower()
|
|
||||||
|
|
||||||
rfid = self.input_str(
|
|
||||||
"RFID (optional)",
|
|
||||||
regex=User.rfid_re,
|
|
||||||
length_range=(0, 10),
|
|
||||||
empty_string_is_none=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User(username, cardnum, rfid)
|
user = User(username, cardnum, rfid)
|
||||||
self.sql_session.add(user)
|
self.session.add(user)
|
||||||
try:
|
try:
|
||||||
self.sql_session.commit()
|
self.session.commit()
|
||||||
print(f"User {username} stored")
|
print(f"User {username} stored")
|
||||||
except IntegrityError as e:
|
except sqlalchemy.exc.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, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Edit user", sql_session)
|
Menu.__init__(self, "Edit user", uses_db=True)
|
||||||
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.
|
||||||
|
|
||||||
@@ -66,7 +47,7 @@ First select an existing user, then enter a new card number for that
|
|||||||
user, then rfid (write an empty line to remove the card number or rfid).
|
user, then rfid (write an empty line to remove the card number or rfid).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
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}")
|
||||||
@@ -88,50 +69,43 @@ 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.sql_session.commit()
|
self.session.commit()
|
||||||
print(f"User {user.name} stored")
|
print(f"User {user.name} stored")
|
||||||
except SQLAlchemyError as e:
|
except sqlalchemy.exc.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, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Add product", sql_session)
|
Menu.__init__(self, "Add product", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
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.sql_session.add(product)
|
self.session.add(product)
|
||||||
try:
|
try:
|
||||||
self.sql_session.commit()
|
self.session.commit()
|
||||||
print(f"Product {name} stored")
|
print(f"Product {name} stored")
|
||||||
except SQLAlchemyError as e:
|
except sqlalchemy.exc.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, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Edit product", sql_session)
|
Menu.__init__(self, "Edit product", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
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"),
|
||||||
@@ -161,10 +135,9 @@ 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.sql_session.commit()
|
self.session.commit()
|
||||||
print(f"Product {product.name} stored")
|
print(f"Product {product.name} stored")
|
||||||
except SQLAlchemyError as e:
|
except sqlalchemy.exc.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
|
||||||
@@ -176,10 +149,10 @@ class EditProductMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class AdjustStockMenu(Menu):
|
class AdjustStockMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Adjust stock", sql_session)
|
Menu.__init__(self, "Adjust stock", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
product = self.input_product("Product")
|
product = self.input_product("Product")
|
||||||
|
|
||||||
@@ -190,16 +163,15 @@ class AdjustStockMenu(Menu):
|
|||||||
if add_stock > 0:
|
if add_stock > 0:
|
||||||
print(f"You added {add_stock:d} to the stock of {product}")
|
print(f"You added {add_stock:d} to the stock of {product}")
|
||||||
else:
|
else:
|
||||||
print(f"You removed {(add_stock * -1):d} from the stock of {product}")
|
print(f"You removed {add_stock:d} from the stock of {product}")
|
||||||
|
|
||||||
product.stock += add_stock
|
product.stock += add_stock
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.sql_session.commit()
|
self.session.commit()
|
||||||
print("Stock is now stored")
|
print("Stock is now stored")
|
||||||
self.pause()
|
self.pause()
|
||||||
except SQLAlchemyError as e:
|
except sqlalchemy.exc.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
|
||||||
@@ -207,13 +179,13 @@ class AdjustStockMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class CleanupStockMenu(Menu):
|
class CleanupStockMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Stock Cleanup", sql_session)
|
Menu.__init__(self, "Stock Cleanup", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
|
|
||||||
products = self.sql_session.query(Product).filter(Product.stock != 0).all()
|
products = self.session.query(Product).filter(Product.stock != 0).all()
|
||||||
|
|
||||||
print("Every product in stock will be printed.")
|
print("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.")
|
||||||
@@ -227,16 +199,15 @@ 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.sql_session.add(product)
|
self.session.add(product)
|
||||||
if oldstock != product.stock:
|
if oldstock != product.stock:
|
||||||
changed_products.append((product, oldstock))
|
changed_products.append((product, oldstock))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.sql_session.commit()
|
self.session.commit()
|
||||||
print("New stocks are now stored.")
|
print("New stocks are now stored.")
|
||||||
self.pause()
|
self.pause()
|
||||||
except SQLAlchemyError as e:
|
except sqlalchemy.exc.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
|
||||||
|
|||||||
+70
-87
@@ -1,146 +1,129 @@
|
|||||||
from textwrap import dedent
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from .helpermenus import MessageMenu, Menu
|
||||||
|
|
||||||
from .helpermenus import Menu, MessageMenu
|
|
||||||
|
|
||||||
|
|
||||||
class FAQMenu(Menu):
|
class FAQMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Frequently Asked Questions", sql_session)
|
Menu.__init__(self, "Frequently Asked Questions")
|
||||||
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
|
||||||
you buy eventually).
|
you buy eventually).
|
||||||
|
|
||||||
Dibbler stores a "credit" amount for each user. When you register a
|
Dibbler stores a "credit" amount for each user. When you register a
|
||||||
purchase in Dibbler, this amount is decreased. To increase your
|
purchase in Dibbler, this amount is decreased. To increase your
|
||||||
credit, purchase products for dibbler, and register them using "Add
|
credit, purchase products for dibbler, and register them using "Add
|
||||||
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.
|
||||||
|
|
||||||
It is also used by Dibbler to indicate that the program wants you to
|
It is also used by Dibbler to indicate that the program wants you to
|
||||||
read some text before continuing. Whenever some output ends with a
|
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
|
||||||
should fix it (or better: force someone else to do it).
|
should fix it (or better: force someone else to do it).
|
||||||
|
|
||||||
Follow this procedure:
|
Follow this procedure:
|
||||||
|
|
||||||
1. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
|
1. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
|
||||||
|
|
||||||
2. Fix the bug.
|
2. Fix the bug.
|
||||||
|
|
||||||
3. Check that the program still runs (and, preferably, that the bug is
|
3. Check that the program still runs (and, preferably, that the bug is
|
||||||
in fact fixed).
|
in fact fixed).
|
||||||
|
|
||||||
4. Commit.
|
4. Commit.
|
||||||
|
|
||||||
5. Update the running copy from svn:
|
5. Update the running copy from svn:
|
||||||
|
|
||||||
$ su -
|
$ su -
|
||||||
# su -l -s /bin/bash pvvvv
|
# su -l -s /bin/bash pvvvv
|
||||||
$ cd dibbler
|
$ cd dibbler
|
||||||
$ git pull
|
$ git pull
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
1. Ask someone (or read the source code) and get an answer.
|
1. Ask someone (or read the source code) and get an answer.
|
||||||
|
|
||||||
2. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
|
2. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
|
||||||
|
|
||||||
3. Add your question (with answer) to the FAQ and commit.
|
3. Add your question (with answer) to the FAQ and commit.
|
||||||
|
|
||||||
4. Update the running copy from svn:
|
4. Update the running copy from svn:
|
||||||
|
|
||||||
$ su -
|
$ su -
|
||||||
# su -l -s /bin/bash pvvvv
|
# su -l -s /bin/bash pvvvv
|
||||||
$ cd dibbler
|
$ cd dibbler
|
||||||
$ git pull
|
$ git pull
|
||||||
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
+166
-280
@@ -1,64 +1,44 @@
|
|||||||
from __future__ import annotations
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
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 (
|
||||||
argmax,
|
|
||||||
guess_data_type,
|
|
||||||
search_product,
|
|
||||||
search_user,
|
search_user,
|
||||||
|
search_product,
|
||||||
|
guess_data_type,
|
||||||
|
argmax,
|
||||||
)
|
)
|
||||||
from dibbler.models import Product, User
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
exit_commands = ["exit", "abort", "quit", "bye", "eat flaming death", "q"]
|
||||||
from collections.abc import Callable, Iterable
|
help_commands = ["help", "?"]
|
||||||
|
context_commands = ["what", "??"]
|
||||||
from sqlalchemy.orm import Session
|
local_help_commands = ["help!", "???"]
|
||||||
|
|
||||||
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 ExitMenuException(Exception):
|
class ExitMenu(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
MenuItemType = TypeVar("MenuItemType", bound="Menu")
|
class Menu(object):
|
||||||
|
|
||||||
|
|
||||||
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: str,
|
name,
|
||||||
sql_session: Session,
|
items=None,
|
||||||
items: list[Self | tuple[MenuItemType, str] | str] | None = None,
|
prompt=None,
|
||||||
prompt: str | None = None,
|
end_prompt="> ",
|
||||||
end_prompt: str | None = "> ",
|
return_index=True,
|
||||||
return_index: bool = True,
|
exit_msg=None,
|
||||||
exit_msg: str | None = None,
|
exit_confirm_msg=None,
|
||||||
exit_confirm_msg: str | None = None,
|
exit_disallowed_msg=None,
|
||||||
exit_disallowed_msg: str | None = None,
|
help_text=None,
|
||||||
help_text: str | None = None,
|
uses_db=False,
|
||||||
) -> None:
|
):
|
||||||
self.name: str = name
|
self.name = 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
|
||||||
@@ -68,61 +48,54 @@ class Menu:
|
|||||||
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
|
||||||
|
|
||||||
assert name is not None
|
def exit_menu(self):
|
||||||
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 ExitMenuException()
|
raise ExitMenu()
|
||||||
|
|
||||||
def at_exit(self) -> None:
|
def at_exit(self):
|
||||||
if self.exit_msg:
|
if self.exit_msg:
|
||||||
print(self.exit_msg)
|
print(self.exit_msg)
|
||||||
|
|
||||||
def set_context(
|
def set_context(self, string, display=True):
|
||||||
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: str) -> None:
|
def add_to_context(self, string):
|
||||||
if self.context is not None:
|
self.context += string
|
||||||
self.context += string
|
|
||||||
else:
|
|
||||||
self.context = string
|
|
||||||
|
|
||||||
def printc(self, string: str) -> None:
|
def printc(self, string):
|
||||||
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) -> None:
|
def show_context(self):
|
||||||
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: int) -> bool:
|
def item_is_submenu(self, i):
|
||||||
return isinstance(self.items[i], Menu)
|
return isinstance(self.items[i], Menu)
|
||||||
|
|
||||||
def item_name(self, i: int) -> str:
|
def item_name(self, i):
|
||||||
if self.item_is_submenu(i):
|
if self.item_is_submenu(i):
|
||||||
return self.items[i].name
|
return self.items[i].name
|
||||||
if isinstance(self.items[i], tuple):
|
elif isinstance(self.items[i], tuple):
|
||||||
return self.items[i][1]
|
return self.items[i][1]
|
||||||
return self.items[i]
|
else:
|
||||||
|
return self.items[i]
|
||||||
|
|
||||||
def item_value(self, i: int) -> MenuItemType | int:
|
def item_value(self, i):
|
||||||
if isinstance(self.items[i], tuple):
|
if isinstance(self.items[i], tuple):
|
||||||
return self.items[i][0]
|
return self.items[i][0]
|
||||||
if self.return_index:
|
if self.return_index:
|
||||||
@@ -131,14 +104,14 @@ class Menu:
|
|||||||
|
|
||||||
def input_str(
|
def input_str(
|
||||||
self,
|
self,
|
||||||
prompt: str | None = None,
|
prompt=None,
|
||||||
end_prompt: str | None = None,
|
end_prompt=None,
|
||||||
regex: str | None = None,
|
regex=None,
|
||||||
length_range: tuple[int | None, int | None] = (None, None),
|
length_range=(None, None),
|
||||||
empty_string_is_none: bool = False,
|
empty_string_is_none=False,
|
||||||
timeout: int | None = None,
|
timeout=None,
|
||||||
default: str | None = None,
|
default=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:
|
||||||
@@ -195,7 +168,7 @@ class Menu:
|
|||||||
):
|
):
|
||||||
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}")
|
||||||
@@ -204,7 +177,7 @@ class Menu:
|
|||||||
continue
|
continue
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def special_input_options(self, result) -> bool:
|
def special_input_options(self, result):
|
||||||
"""
|
"""
|
||||||
Handles special, magic input for input_str
|
Handles special, magic input for input_str
|
||||||
|
|
||||||
@@ -214,7 +187,7 @@ class Menu:
|
|||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def special_input_choice(self, in_str: str) -> bool:
|
def special_input_choice(self, in_str):
|
||||||
"""
|
"""
|
||||||
Handle choices which are not simply menu items.
|
Handle choices which are not simply menu items.
|
||||||
|
|
||||||
@@ -224,39 +197,33 @@ class Menu:
|
|||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def input_choice(
|
def input_choice(self, number_of_choices, prompt=None, end_prompt=None):
|
||||||
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 number_of_choices >= 10:
|
if choice == 0 and 10 <= number_of_choices:
|
||||||
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: str) -> None:
|
def invalid_menu_choice(self, in_str):
|
||||||
print("Please enter a valid choice.")
|
print("Please enter a valid choice.")
|
||||||
|
|
||||||
def input_int(
|
def input_int(
|
||||||
self,
|
self,
|
||||||
prompt: str,
|
prompt=None,
|
||||||
minimum: int | None = None,
|
minimum=None,
|
||||||
maximum: int | None = None,
|
maximum=None,
|
||||||
null_allowed: bool = False,
|
null_allowed=False,
|
||||||
zero_allowed: bool = True,
|
zero_allowed=True,
|
||||||
default: int | None = None,
|
default=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:
|
||||||
@@ -267,11 +234,7 @@ class Menu:
|
|||||||
end_prompt = ""
|
end_prompt = ""
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
result = self.input_str(
|
result = self.input_str(prompt + end_prompt, default=default)
|
||||||
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:
|
||||||
@@ -289,117 +252,93 @@ class Menu:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
print("Please enter an integer")
|
print("Please enter an integer")
|
||||||
|
|
||||||
def input_user(
|
def input_user(self, prompt=None, end_prompt=None):
|
||||||
self,
|
|
||||||
prompt: str | None = None,
|
|
||||||
end_prompt: str | None = None,
|
|
||||||
) -> User:
|
|
||||||
user = None
|
user = None
|
||||||
while user is None:
|
while user is None:
|
||||||
search_string = self.input_str(prompt, end_prompt, empty_string_is_none = True)
|
user = self.retrieve_user(self.input_str(prompt, end_prompt))
|
||||||
if search_string is None:
|
|
||||||
print("Please write something")
|
|
||||||
continue
|
|
||||||
user = self.retrieve_user(search_string)
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def retrieve_user(self, search_str: str) -> User | None:
|
def retrieve_user(self, search_str):
|
||||||
return self.search_ui(search_user, search_str, "user")
|
return self.search_ui(search_user, search_str, "user")
|
||||||
|
|
||||||
def input_product(
|
def input_product(self, prompt=None, end_prompt=None):
|
||||||
self,
|
|
||||||
prompt: str | None = None,
|
|
||||||
end_prompt: str | None = None,
|
|
||||||
) -> Product:
|
|
||||||
product = None
|
product = None
|
||||||
while product is None:
|
while product is None:
|
||||||
search_string = self.input_str(prompt, end_prompt)
|
product = self.retrieve_product(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: str) -> Product | None:
|
def retrieve_product(self, search_str):
|
||||||
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: str | None = None,
|
prompt=None,
|
||||||
end_prompt: str | None = None,
|
end_prompt=None,
|
||||||
permitted_things: Iterable[str] = ("user", "product"),
|
permitted_things=("user", "product"),
|
||||||
add_nonexisting: Iterable[str] = (),
|
add_nonexisting=(),
|
||||||
empty_input_permitted: bool = False,
|
empty_input_permitted=False,
|
||||||
find_hidden_products: bool = True,
|
find_hidden_products=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,
|
search_str, permitted_things, add_nonexisting, find_hidden_products
|
||||||
permitted_things,
|
|
||||||
add_nonexisting,
|
|
||||||
find_hidden_products,
|
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def input_multiple(
|
def input_multiple(
|
||||||
self,
|
self,
|
||||||
prompt: str | None = None,
|
prompt=None,
|
||||||
end_prompt: str | None = None,
|
end_prompt=None,
|
||||||
permitted_things: Iterable[str] = ("user", "product"),
|
permitted_things=("user", "product"),
|
||||||
add_nonexisting: Iterable[str] = (),
|
add_nonexisting=(),
|
||||||
empty_input_permitted: bool = False,
|
empty_input_permitted=False,
|
||||||
find_hidden_products: bool = True,
|
find_hidden_products=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
|
||||||
result = self.search_for_thing(
|
else:
|
||||||
search_str,
|
result = self.search_for_thing(
|
||||||
permitted_things,
|
search_str, permitted_things, add_nonexisting, find_hidden_products
|
||||||
add_nonexisting,
|
)
|
||||||
find_hidden_products,
|
num = 1
|
||||||
)
|
|
||||||
num = 1
|
|
||||||
|
|
||||||
if (result is None) and (len(search_lst) > 1):
|
if (result is None) and (len(search_lst) > 1):
|
||||||
print('Interpreting input as "<number> <product>"')
|
print('Interpreting input as "<number> <product>"')
|
||||||
try:
|
try:
|
||||||
num = int(search_lst[0])
|
num = int(search_lst[0])
|
||||||
result = self.search_for_thing(
|
result = self.search_for_thing(
|
||||||
" ".join(search_lst[1:]),
|
" ".join(search_lst[1:]),
|
||||||
permitted_things,
|
permitted_things,
|
||||||
add_nonexisting,
|
add_nonexisting,
|
||||||
find_hidden_products,
|
find_hidden_products,
|
||||||
)
|
)
|
||||||
# Her kan det legges inn en except ValueError,
|
# Her kan det legges inn en except ValueError,
|
||||||
# men da blir det fort mye plaging av brukeren
|
# men da blir det fort mye plaging av brukeren
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return result, num
|
return result, num
|
||||||
|
|
||||||
def search_for_thing(
|
def search_for_thing(
|
||||||
self,
|
self,
|
||||||
search_str: str,
|
search_str,
|
||||||
permitted_things: Iterable[str] = ("user", "product"),
|
permitted_things=("user", "product"),
|
||||||
add_non_existing: Iterable[str] = (),
|
add_non_existing=(),
|
||||||
find_hidden_products: bool = True,
|
find_hidden_products=True,
|
||||||
) -> User | Product | None:
|
):
|
||||||
search_fun = {
|
search_fun = {"user": search_user, "product": search_product}
|
||||||
"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.sql_session, find_hidden_products)
|
results[thing] = search_fun[thing](search_str, self.session, find_hidden_products)
|
||||||
result_values[thing] = self.search_result_value(results[thing])
|
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]:
|
||||||
@@ -414,14 +353,10 @@ class Menu:
|
|||||||
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(
|
return self.search_ui2(search_str, results[selected_thing], selected_thing)
|
||||||
search_str,
|
|
||||||
results[selected_thing],
|
|
||||||
selected_thing,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_result_value(result) -> Literal[0, 1, 2, 3]:
|
def search_result_value(result):
|
||||||
if result is None:
|
if result is None:
|
||||||
return 0
|
return 0
|
||||||
if not isinstance(result, list):
|
if not isinstance(result, list):
|
||||||
@@ -432,19 +367,18 @@ class Menu:
|
|||||||
return 2
|
return 2
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def search_add(self, string: str) -> User | None:
|
def search_add(self, string):
|
||||||
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.sql_session.add(user)
|
self.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}"),
|
||||||
@@ -453,14 +387,12 @@ class Menu:
|
|||||||
selection = selector.execute()
|
selection = selector.execute()
|
||||||
if selection == "create":
|
if selection == "create":
|
||||||
username = self.input_str(
|
username = self.input_str(
|
||||||
prompt="Username for new user (should be same as PVV username)",
|
"Username for new user (should be same as PVV username)",
|
||||||
end_prompt=None,
|
User.name_re,
|
||||||
regex=User.name_re,
|
(1, 10),
|
||||||
length_range=(1, 10),
|
|
||||||
)
|
)
|
||||||
assert username is not None
|
|
||||||
user = User(username, string)
|
user = User(username, string)
|
||||||
self.sql_session.add(user)
|
self.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")
|
||||||
@@ -473,21 +405,11 @@ class Menu:
|
|||||||
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(
|
def search_ui(self, search_fun, search_str, thing):
|
||||||
self,
|
result = search_fun(search_str, self.session)
|
||||||
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(
|
def search_ui2(self, search_str, result, thing):
|
||||||
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:
|
||||||
@@ -507,41 +429,25 @@ class Menu:
|
|||||||
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(
|
selector = Selector(select_header, items=select_items, return_index=False)
|
||||||
select_header,
|
|
||||||
self.sql_session,
|
|
||||||
items=select_items,
|
|
||||||
return_index=False,
|
|
||||||
)
|
|
||||||
return selector.execute()
|
return selector.execute()
|
||||||
|
|
||||||
def confirm(
|
@staticmethod
|
||||||
self,
|
def confirm(prompt, end_prompt=None, default=None, timeout=None):
|
||||||
prompt: str,
|
return ConfirmMenu(prompt, end_prompt=None, default=default, timeout=timeout).execute()
|
||||||
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) -> str:
|
def header(self):
|
||||||
return f"[{self.name}]"
|
return f"[{self.name}]"
|
||||||
|
|
||||||
def print_header(self) -> None:
|
def print_header(self):
|
||||||
print("")
|
print("")
|
||||||
print(self.header())
|
print(self.header())
|
||||||
|
|
||||||
def pause(self) -> None:
|
def pause(self):
|
||||||
self.input_str(".", end_prompt="")
|
self.input_str(".", end_prompt="")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def general_help() -> None:
|
def general_help():
|
||||||
print(
|
print(
|
||||||
"""
|
"""
|
||||||
DIBBLER HELP
|
DIBBLER HELP
|
||||||
@@ -564,10 +470,10 @@ class Menu:
|
|||||||
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) -> None:
|
def local_help(self):
|
||||||
if self.help_text is None:
|
if self.help_text is None:
|
||||||
print("no help here")
|
print("no help here")
|
||||||
else:
|
else:
|
||||||
@@ -575,15 +481,21 @@ class Menu:
|
|||||||
print(f"Help for {self.header()}:")
|
print(f"Help for {self.header()}:")
|
||||||
print(self.help_text)
|
print(self.help_text)
|
||||||
|
|
||||||
def execute(self, **_kwargs) -> MenuItemType | int | None:
|
def execute(self, **kwargs):
|
||||||
self.set_context(None)
|
self.set_context(None)
|
||||||
try:
|
try:
|
||||||
return self._execute(**_kwargs)
|
if self.uses_db and not self.session:
|
||||||
except ExitMenuException:
|
self.session = Session()
|
||||||
|
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) -> MenuItemType | int | None:
|
def _execute(self, **kwargs):
|
||||||
while True:
|
while True:
|
||||||
self.print_header()
|
self.print_header()
|
||||||
self.set_context(None)
|
self.set_context(None)
|
||||||
@@ -602,21 +514,12 @@ class Menu:
|
|||||||
|
|
||||||
|
|
||||||
class MessageMenu(Menu):
|
class MessageMenu(Menu):
|
||||||
message: str
|
def __init__(self, name, message, pause_after_message=True):
|
||||||
pause_after_message: bool
|
Menu.__init__(self, name)
|
||||||
|
|
||||||
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, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
print("")
|
print("")
|
||||||
print(self.message)
|
print(self.message)
|
||||||
@@ -625,17 +528,10 @@ class MessageMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class ConfirmMenu(Menu):
|
class ConfirmMenu(Menu):
|
||||||
def __init__(
|
def __init__(self, prompt="confirm? ", end_prompt=": ", default=None, timeout=0):
|
||||||
self,
|
Menu.__init__(
|
||||||
sql_session: Session,
|
self,
|
||||||
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",
|
||||||
@@ -643,55 +539,45 @@ class ConfirmMenu(Menu):
|
|||||||
self.default = default
|
self.default = default
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> bool:
|
def _execute(self):
|
||||||
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}",
|
f"{self.prompt} ({options})", end_prompt=": ", timeout=self.timeout
|
||||||
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
|
||||||
if result in ["n", "no"]:
|
elif result in ["n", "no"]:
|
||||||
return False
|
return False
|
||||||
if self.default is not None and result == "":
|
elif self.default is not None and result == "":
|
||||||
return self.default
|
return self.default
|
||||||
print("Please answer yes or no")
|
else:
|
||||||
|
print("Please answer yes or no")
|
||||||
|
|
||||||
|
|
||||||
class Selector(Menu):
|
class Selector(Menu):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name,
|
||||||
sql_session: Session,
|
items=None,
|
||||||
items: list[Self | tuple[MenuItemType, str] | str] | None = None,
|
prompt="select",
|
||||||
prompt: str | None = "select",
|
return_index=True,
|
||||||
return_index: bool = True,
|
exit_msg=None,
|
||||||
exit_msg: str | None = None,
|
exit_confirm_msg=None,
|
||||||
exit_confirm_msg: str | None = None,
|
help_text=None,
|
||||||
help_text: str | None = None,
|
):
|
||||||
) -> None:
|
|
||||||
if items is None:
|
if items is None:
|
||||||
items = []
|
items = []
|
||||||
super().__init__(
|
Menu.__init__(self, name, items, prompt, return_index=return_index, exit_msg=exit_msg)
|
||||||
name,
|
|
||||||
sql_session,
|
|
||||||
items,
|
|
||||||
prompt,
|
|
||||||
return_index=return_index,
|
|
||||||
exit_msg=exit_msg,
|
|
||||||
help_text=help_text,
|
|
||||||
)
|
|
||||||
|
|
||||||
def header(self) -> str:
|
def header(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def print_header(self) -> None:
|
def print_header(self):
|
||||||
print(self.header())
|
print(self.header())
|
||||||
|
|
||||||
def local_help(self) -> None:
|
def local_help(self):
|
||||||
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.")
|
||||||
|
|||||||
+20
-16
@@ -1,8 +1,9 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from dibbler.db import Session
|
||||||
|
|
||||||
from .buymenu import BuyMenu
|
from .buymenu import BuyMenu
|
||||||
from .faq import FAQMenu
|
from .faq import FAQMenu
|
||||||
@@ -12,17 +13,14 @@ faq_commands = ["faq"]
|
|||||||
restart_commands = ["restart"]
|
restart_commands = ["restart"]
|
||||||
|
|
||||||
|
|
||||||
def restart() -> None:
|
def restart():
|
||||||
# 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 __init__(self, sql_session: Session, **_kwargs) -> None:
|
def special_input_choice(self, in_str):
|
||||||
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])
|
||||||
@@ -30,7 +28,7 @@ class MainMenu(Menu):
|
|||||||
else:
|
else:
|
||||||
num = 1
|
num = 1
|
||||||
item_name = in_str
|
item_name = in_str
|
||||||
buy_menu = BuyMenu(self.sql_session)
|
buy_menu = BuyMenu(Session())
|
||||||
thing = buy_menu.search_for_thing(item_name, find_hidden_products=False)
|
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)])
|
||||||
@@ -38,26 +36,32 @@ class MainMenu(Menu):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def special_input_options(self, result: str) -> bool:
|
def special_input_options(self, result):
|
||||||
if result in faq_commands:
|
if result in faq_commands:
|
||||||
FAQMenu(self.sql_session).execute()
|
FAQMenu().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
|
||||||
if result == "c":
|
elif result == "c":
|
||||||
print(f"\033[{random.randint(40, 49)};{random.randint(30, 37)};5m")
|
os.system(
|
||||||
print("\033[2J")
|
'echo -e "\033['
|
||||||
|
+ str(random.randint(40, 49))
|
||||||
|
+ ";"
|
||||||
|
+ str(random.randint(30, 37))
|
||||||
|
+ ';5m"'
|
||||||
|
)
|
||||||
|
os.system("clear")
|
||||||
self.show_context()
|
self.show_context()
|
||||||
return True
|
return True
|
||||||
if result == "cs":
|
elif result == "cs":
|
||||||
print("\033[0m")
|
os.system('echo -e "\033[0m"')
|
||||||
print("\033[2J")
|
os.system("clear")
|
||||||
self.show_context()
|
self.show_context()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def invalid_menu_choice(self, in_str: str) -> None:
|
def invalid_menu_choice(self, in_str):
|
||||||
print(self.show_context())
|
print(self.show_context())
|
||||||
|
|||||||
+46
-49
@@ -1,19 +1,17 @@
|
|||||||
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, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Transfer credit between users", sql_session)
|
Menu.__init__(self, "Transfer credit between users", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
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)
|
||||||
@@ -28,25 +26,24 @@ 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.sql_session.add(t1)
|
self.session.add(t1)
|
||||||
self.sql_session.add(t2)
|
self.session.add(t2)
|
||||||
try:
|
try:
|
||||||
self.sql_session.commit()
|
self.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 SQLAlchemyError as e:
|
except sqlalchemy.exc.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, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Show user", sql_session)
|
Menu.__init__(self, "Show user", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
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}")
|
||||||
@@ -55,12 +52,11 @@ 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["limits"]["user_recent_transaction_limit"])
|
+ str(config.getint("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"),
|
||||||
@@ -69,7 +65,7 @@ class ShowUserMenu(Menu):
|
|||||||
)
|
)
|
||||||
what = selector.execute()
|
what = selector.execute()
|
||||||
if what == "transactions":
|
if what == "transactions":
|
||||||
self.print_transactions(user, config["limits"]["user_recent_transaction_limit"])
|
self.print_transactions(user, config.getint("limits", "user_recent_transaction_limit"))
|
||||||
elif what == "products":
|
elif what == "products":
|
||||||
self.print_purchased_products(user)
|
self.print_purchased_products(user)
|
||||||
elif what == "transactions-all":
|
elif what == "transactions-all":
|
||||||
@@ -78,7 +74,7 @@ class ShowUserMenu(Menu):
|
|||||||
print("What what?")
|
print("What what?")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def print_transactions(user: User, limit: int | None = None) -> None:
|
def print_transactions(user, limit=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
|
||||||
@@ -91,7 +87,10 @@ class ShowUserMenu(Menu):
|
|||||||
if t.purchase:
|
if t.purchase:
|
||||||
products = []
|
products = []
|
||||||
for entry in t.purchase.entries:
|
for entry in t.purchase.entries:
|
||||||
amount = f"{abs(entry.amount)}x " if abs(entry.amount) != 1 else ""
|
if abs(entry.amount) != 1:
|
||||||
|
amount = f"{abs(entry.amount)}x "
|
||||||
|
else:
|
||||||
|
amount = ""
|
||||||
product = f"{amount}{entry.product.name}"
|
product = f"{amount}{entry.product.name}"
|
||||||
products.append(product)
|
products.append(product)
|
||||||
string += "purchase ("
|
string += "purchase ("
|
||||||
@@ -99,13 +98,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"
|
||||||
elif t.description is not None:
|
else:
|
||||||
string += t.description
|
string += t.description
|
||||||
string += "\n"
|
string += "\n"
|
||||||
less(string)
|
less(string)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def print_purchased_products(user: User) -> None:
|
def print_purchased_products(user):
|
||||||
products = []
|
products = []
|
||||||
for ref in user.products:
|
for ref in user.products:
|
||||||
product = ref.product
|
product = ref.product
|
||||||
@@ -124,13 +123,13 @@ class ShowUserMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class UserListMenu(Menu):
|
class UserListMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("User list", sql_session)
|
Menu.__init__(self, "User list", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
user_list = self.sql_session.query(User).all()
|
user_list = self.session.query(User).all()
|
||||||
total_credit = self.sql_session.query(sqlalchemy.func.sum(User.credit)).first()[0]
|
total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0]
|
||||||
|
|
||||||
line_format = "%-12s | %6s\n"
|
line_format = "%-12s | %6s\n"
|
||||||
hline = "---------------------\n"
|
hline = "---------------------\n"
|
||||||
@@ -145,10 +144,10 @@ class UserListMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class AdjustCreditMenu(Menu):
|
class AdjustCreditMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Adjust credit", sql_session)
|
Menu.__init__(self, "Adjust credit", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
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")
|
||||||
@@ -165,25 +164,24 @@ 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.sql_session.add(transaction)
|
self.session.add(transaction)
|
||||||
try:
|
try:
|
||||||
self.sql_session.commit()
|
self.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 SQLAlchemyError as e:
|
except sqlalchemy.exc.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, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Product list", sql_session)
|
Menu.__init__(self, "Product list", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
text = ""
|
text = ""
|
||||||
product_list = (
|
product_list = (
|
||||||
self.sql_session.query(Product)
|
self.session.query(Product)
|
||||||
.filter(Product.hidden.is_(False))
|
.filter(Product.hidden.is_(False))
|
||||||
.order_by(Product.stock.desc())
|
.order_by(Product.stock.desc())
|
||||||
)
|
)
|
||||||
@@ -206,22 +204,21 @@ class ProductListMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class ProductSearchMenu(Menu):
|
class ProductSearchMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Product search", sql_session)
|
Menu.__init__(self, "Product search", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
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(
|
||||||
", ".join(
|
"Result: %s, price: %d kr, bar code: %s, stock: %d, hidden: %s"
|
||||||
[
|
% (
|
||||||
f"Result: {product.name}",
|
product.name,
|
||||||
f"price: {product.price} kr",
|
product.price,
|
||||||
f"bar code: {product.bar_code}",
|
product.bar_code,
|
||||||
f"stock: {product.stock}",
|
product.stock,
|
||||||
f"hidden: {'Y' if product.hidden else 'N'}",
|
("Y" if product.hidden else "N"),
|
||||||
],
|
)
|
||||||
),
|
|
||||||
)
|
)
|
||||||
# self.pause()
|
# self.pause()
|
||||||
|
|||||||
@@ -1,46 +1,45 @@
|
|||||||
from sqlalchemy.orm import Session
|
import re
|
||||||
|
|
||||||
|
from dibbler.conf import config
|
||||||
|
from dibbler.models import Product, User
|
||||||
|
from dibbler.lib.printer_helpers import print_bar_code, print_name_label
|
||||||
|
|
||||||
# from dibbler.lib.printer_helpers import print_bar_code, print_name_label
|
|
||||||
from .helpermenus import Menu
|
from .helpermenus import Menu
|
||||||
|
|
||||||
|
|
||||||
class PrintLabelMenu(Menu):
|
class PrintLabelMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Print a label", sql_session)
|
Menu.__init__(self, "Print a label", uses_db=True)
|
||||||
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, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
|
|
||||||
print("Printer menu is under renovation, please be patient")
|
thing = self.input_thing("Product/User")
|
||||||
|
|
||||||
return
|
if isinstance(thing, Product):
|
||||||
|
if re.match(r"^[0-9]{13}$", thing.bar_code):
|
||||||
# thing = self.input_thing("Product/User")
|
bar_type = "ean13"
|
||||||
|
elif re.match(r"^[0-9]{8}$", thing.bar_code):
|
||||||
# if isinstance(thing, Product):
|
bar_type = "ean8"
|
||||||
# if re.match(r"^[0-9]{13}$", thing.bar_code):
|
else:
|
||||||
# bar_type = "ean13"
|
bar_type = "code39"
|
||||||
# elif re.match(r"^[0-9]{8}$", thing.bar_code):
|
print_bar_code(
|
||||||
# bar_type = "ean8"
|
thing.bar_code,
|
||||||
# else:
|
thing.name,
|
||||||
# bar_type = "code39"
|
barcode_type=bar_type,
|
||||||
# print_bar_code(
|
rotate=config.getboolean("printer", "rotate"),
|
||||||
# thing.bar_code,
|
printer_type="QL-700",
|
||||||
# thing.name,
|
label_type=config.get("printer", "label_type"),
|
||||||
# barcode_type=bar_type,
|
)
|
||||||
# rotate=config["printer"]["rotate"],
|
elif isinstance(thing, User):
|
||||||
# printer_type="QL-700",
|
print_name_label(
|
||||||
# label_type=config.get("printer", "label_type"),
|
text=thing.name,
|
||||||
# )
|
label_type=config.get("printer", "label_type"),
|
||||||
# elif isinstance(thing, User):
|
rotate=config.getboolean("printer", "rotate"),
|
||||||
# print_name_label(
|
printer_type="QL-700",
|
||||||
# text=thing.name,
|
)
|
||||||
# label_type=config["printer"]["label_type"],
|
|
||||||
# rotate=config["printer"]["rotate"],
|
|
||||||
# printer_type="QL-700",
|
|
||||||
# )
|
|
||||||
|
|||||||
+23
-28
@@ -1,9 +1,8 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -16,14 +15,14 @@ __all__ = [
|
|||||||
|
|
||||||
|
|
||||||
class ProductPopularityMenu(Menu):
|
class ProductPopularityMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Products by popularity", sql_session)
|
Menu.__init__(self, "Products by popularity", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
text = ""
|
text = ""
|
||||||
sub = (
|
sub = (
|
||||||
self.sql_session.query(
|
self.session.query(
|
||||||
PurchaseEntry.product_id,
|
PurchaseEntry.product_id,
|
||||||
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
||||||
)
|
)
|
||||||
@@ -32,8 +31,8 @@ class ProductPopularityMenu(Menu):
|
|||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
product_list = (
|
product_list = (
|
||||||
self.sql_session.query(Product, sub.c.purchase_count)
|
self.session.query(Product, sub.c.purchase_count)
|
||||||
.outerjoin(sub, Product.product_id == sub.c.product_id)
|
.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()
|
||||||
@@ -49,14 +48,14 @@ class ProductPopularityMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class ProductRevenueMenu(Menu):
|
class ProductRevenueMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Products by revenue", sql_session)
|
Menu.__init__(self, "Products by revenue", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
text = ""
|
text = ""
|
||||||
sub = (
|
sub = (
|
||||||
self.sql_session.query(
|
self.session.query(
|
||||||
PurchaseEntry.product_id,
|
PurchaseEntry.product_id,
|
||||||
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
func.sum(PurchaseEntry.amount).label("purchase_count"),
|
||||||
)
|
)
|
||||||
@@ -65,8 +64,8 @@ class ProductRevenueMenu(Menu):
|
|||||||
.subquery()
|
.subquery()
|
||||||
)
|
)
|
||||||
product_list = (
|
product_list = (
|
||||||
self.sql_session.query(Product, sub.c.purchase_count)
|
self.session.query(Product, sub.c.purchase_count)
|
||||||
.outerjoin(sub, Product.product_id == sub.c.product_id)
|
.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()
|
||||||
@@ -87,26 +86,22 @@ class ProductRevenueMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class BalanceMenu(Menu):
|
class BalanceMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Total balance of PVVVV", sql_session)
|
Menu.__init__(self, "Total balance of PVVVV", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
self.print_header()
|
self.print_header()
|
||||||
text = ""
|
text = ""
|
||||||
total_value = 0
|
total_value = 0
|
||||||
product_list = self.sql_session.query(Product).filter(Product.stock > 0).all()
|
product_list = self.session.query(Product).filter(Product.stock > 0).all()
|
||||||
for p in product_list:
|
for p in product_list:
|
||||||
total_value += p.stock * p.price
|
total_value += p.stock * p.price
|
||||||
|
|
||||||
total_positive_credit = (
|
total_positive_credit = (
|
||||||
self.sql_session.query(func.coalesce(func.sum(User.credit), 0))
|
self.session.query(func.sum(User.credit)).filter(User.credit > 0).first()[0]
|
||||||
.filter(User.credit > 0)
|
|
||||||
.first()[0]
|
|
||||||
)
|
)
|
||||||
total_negative_credit = (
|
total_negative_credit = (
|
||||||
self.sql_session.query(func.coalesce(func.sum(User.credit), 0))
|
self.session.query(func.sum(User.credit)).filter(User.credit < 0).first()[0]
|
||||||
.filter(User.credit < 0)
|
|
||||||
.first()[0]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
total_credit = total_positive_credit + total_negative_credit
|
total_credit = total_positive_credit + total_negative_credit
|
||||||
@@ -124,8 +119,8 @@ class BalanceMenu(Menu):
|
|||||||
|
|
||||||
|
|
||||||
class LoggedStatisticsMenu(Menu):
|
class LoggedStatisticsMenu(Menu):
|
||||||
def __init__(self, sql_session: Session) -> None:
|
def __init__(self):
|
||||||
super().__init__("Statistics from log", sql_session)
|
Menu.__init__(self, "Statistics from log", uses_db=True)
|
||||||
|
|
||||||
def _execute(self, **_kwargs) -> None:
|
def _execute(self):
|
||||||
statisticsTextOnly(self.sql_session)
|
statisticsTextOnly()
|
||||||
|
|||||||
@@ -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,5 +1,4 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
@@ -37,19 +36,12 @@ class Product(Base):
|
|||||||
name_re = r".+"
|
name_re = r".+"
|
||||||
name_length = 45
|
name_length = 45
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, bar_code, name, price, stock=0, hidden=False):
|
||||||
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) -> str:
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
+14
-14
@@ -1,9 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import math
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
DateTime,
|
DateTime,
|
||||||
Integer,
|
Integer,
|
||||||
@@ -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",
|
back_populates="purchase", order_by="Transaction.user_name"
|
||||||
order_by=Transaction.user_name,
|
|
||||||
)
|
)
|
||||||
entries: Mapped[set[PurchaseEntry]] = relationship(back_populates="purchase")
|
entries: Mapped[set[PurchaseEntry]] = relationship(back_populates="purchase")
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def is_complete(self) -> bool:
|
def is_complete(self):
|
||||||
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: bool = True) -> int:
|
def price_per_transaction(self, round_up=True):
|
||||||
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)))
|
||||||
return int(math.floor(float(self.price) / len(self.transactions)))
|
else:
|
||||||
|
return int(math.floor(float(self.price) / len(self.transactions)))
|
||||||
|
|
||||||
def set_price(self, round_up: bool = True) -> None:
|
def set_price(self, round_up=True):
|
||||||
self.price = 0
|
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: bool = False, round_up: bool = True) -> None:
|
def perform_purchase(self, ignore_penalty=False, round_up=True):
|
||||||
self.time = datetime.now()
|
self.time = datetime.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: int, round_up: bool = True) -> None:
|
def perform_soft_purchase(self, price, round_up=True):
|
||||||
self.time = datetime.now()
|
self.time = datetime.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,10 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
ForeignKey,
|
|
||||||
Integer,
|
Integer,
|
||||||
|
ForeignKey,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
@@ -28,15 +27,10 @@ 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(back_populates="purchases", lazy="joined")
|
product: Mapped[Product] = relationship(lazy="joined")
|
||||||
purchase: Mapped[Purchase] = relationship(back_populates="entries", lazy="joined")
|
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, purchase, product, amount):
|
||||||
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 .Purchase import Purchase
|
|
||||||
from .User import User
|
from .User import User
|
||||||
|
from .Purchase import Purchase
|
||||||
|
|
||||||
|
|
||||||
class Transaction(Base):
|
class Transaction(Base):
|
||||||
@@ -36,24 +36,17 @@ 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(back_populates="transactions", lazy="joined")
|
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, user, amount=0, description=None, purchase=None, penalty=1):
|
||||||
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: bool = False) -> None:
|
def perform_transaction(self, ignore_penalty=False):
|
||||||
self.time = datetime.now()
|
self.time = datetime.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
|
||||||
|
|||||||
+7
-17
@@ -1,5 +1,4 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
@@ -15,34 +14,25 @@ from sqlalchemy.orm import (
|
|||||||
from .Base import Base
|
from .Base import Base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .Transaction import Transaction
|
|
||||||
from .UserProducts import UserProducts
|
from .UserProducts import UserProducts
|
||||||
|
from .Transaction import Transaction
|
||||||
|
|
||||||
|
|
||||||
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[int] = mapped_column(Integer)
|
credit: Mapped[str] = 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[list[UserProducts]] = relationship(back_populates="user")
|
products: Mapped[set[UserProducts]] = relationship(back_populates="user")
|
||||||
transactions: Mapped[list[Transaction]] = relationship(
|
transactions: Mapped[set[Transaction]] = relationship(back_populates="user")
|
||||||
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__(
|
def __init__(self, name, card, rfid=None, credit=0):
|
||||||
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
|
||||||
@@ -52,8 +42,8 @@ class User(Base):
|
|||||||
self.rfid = rfid
|
self.rfid = rfid
|
||||||
self.credit = credit
|
self.credit = credit
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def is_anonymous(self) -> bool:
|
def is_anonymous(self):
|
||||||
return self.card == "11122233"
|
return self.card == "11122233"
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
ForeignKey,
|
|
||||||
Integer,
|
Integer,
|
||||||
|
ForeignKey,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
@@ -15,8 +14,8 @@ from sqlalchemy.orm import (
|
|||||||
from .Base import Base
|
from .Base import Base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .Product import Product
|
|
||||||
from .User import User
|
from .User import User
|
||||||
|
from .Product import Product
|
||||||
|
|
||||||
|
|
||||||
class UserProducts(Base):
|
class UserProducts(Base):
|
||||||
@@ -28,5 +27,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(back_populates="products")
|
user: Mapped[User] = relationship()
|
||||||
product: Mapped[Product] = relationship(back_populates="users")
|
product: Mapped[Product] = relationship()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+38
-70
@@ -1,111 +1,79 @@
|
|||||||
#!/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 ..menus import (
|
from ..lib.helpers import *
|
||||||
AddProductMenu,
|
from ..menus import *
|
||||||
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(sql_session: Session) -> None:
|
def main():
|
||||||
if not config["general"]["stop_allowed"]:
|
if not config.getboolean("general", "stop_allowed"):
|
||||||
set_signal_handler(SIGQUIT, SIG_IGN)
|
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
|
||||||
|
|
||||||
if not config["general"]["stop_allowed"]:
|
if not config.getboolean("general", "stop_allowed"):
|
||||||
set_signal_handler(SIGTSTP, SIG_IGN)
|
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
|
||||||
|
|
||||||
main_menu = MainMenu(
|
main = MainMenu(
|
||||||
sql_session,
|
"Dibbler main menu",
|
||||||
items=[
|
items=[
|
||||||
BuyMenu(sql_session),
|
BuyMenu(),
|
||||||
ProductListMenu(sql_session),
|
ProductListMenu(),
|
||||||
ShowUserMenu(sql_session),
|
ShowUserMenu(),
|
||||||
UserListMenu(sql_session),
|
UserListMenu(),
|
||||||
AdjustCreditMenu(sql_session),
|
AdjustCreditMenu(),
|
||||||
TransferMenu(sql_session),
|
TransferMenu(),
|
||||||
AddStockMenu(sql_session),
|
AddStockMenu(),
|
||||||
Menu(
|
Menu(
|
||||||
"Add/edit",
|
"Add/edit",
|
||||||
sql_session,
|
|
||||||
items=[
|
items=[
|
||||||
AddUserMenu(sql_session),
|
AddUserMenu(),
|
||||||
EditUserMenu(sql_session),
|
EditUserMenu(),
|
||||||
AddProductMenu(sql_session),
|
AddProductMenu(),
|
||||||
EditProductMenu(sql_session),
|
EditProductMenu(),
|
||||||
AdjustStockMenu(sql_session),
|
AdjustStockMenu(),
|
||||||
CleanupStockMenu(sql_session),
|
CleanupStockMenu(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ProductSearchMenu(sql_session),
|
ProductSearchMenu(),
|
||||||
Menu(
|
Menu(
|
||||||
"Statistics",
|
"Statistics",
|
||||||
sql_session,
|
|
||||||
items=[
|
items=[
|
||||||
ProductPopularityMenu(sql_session),
|
ProductPopularityMenu(),
|
||||||
ProductRevenueMenu(sql_session),
|
ProductRevenueMenu(),
|
||||||
BalanceMenu(sql_session),
|
BalanceMenu(),
|
||||||
LoggedStatisticsMenu(sql_session),
|
LoggedStatisticsMenu(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
FAQMenu(sql_session),
|
FAQMenu(),
|
||||||
PrintLabelMenu(sql_session),
|
PrintLabelMenu(),
|
||||||
],
|
],
|
||||||
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["general"]["quit_allowed"]:
|
if not config.getboolean("general", "quit_allowed"):
|
||||||
main_menu.exit_disallowed_msg = (
|
main.exit_disallowed_msg = "You can check out any time you like, but you can never leave."
|
||||||
"You can check out any time you like, but you can never leave."
|
|
||||||
)
|
|
||||||
while True:
|
while True:
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
main_menu.execute()
|
main.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["general"]["show_tracebacks"]:
|
if config.getboolean("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,9 +1,11 @@
|
|||||||
#!/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(engine: Engine) -> None:
|
def main():
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from dibbler.models.Product import Product
|
|
||||||
from dibbler.models.User import User
|
|
||||||
|
|
||||||
JSON_FILE = Path(__file__).parent.parent.parent / "mock_data.json"
|
|
||||||
|
|
||||||
|
|
||||||
def clear_db(sql_session: Session) -> None:
|
|
||||||
sql_session.query(Product).delete()
|
|
||||||
sql_session.query(User).delete()
|
|
||||||
sql_session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def main(sql_session: Session) -> None:
|
|
||||||
clear_db(sql_session)
|
|
||||||
product_items = []
|
|
||||||
user_items = []
|
|
||||||
|
|
||||||
with Path.open(JSON_FILE) as f:
|
|
||||||
json_obj = json.load(f)
|
|
||||||
|
|
||||||
for product in json_obj["products"]:
|
|
||||||
product_item = Product(
|
|
||||||
bar_code=product["bar_code"],
|
|
||||||
name=product["name"],
|
|
||||||
price=product["price"],
|
|
||||||
stock=product["stock"],
|
|
||||||
)
|
|
||||||
product_items.append(product_item)
|
|
||||||
|
|
||||||
for user in json_obj["users"]:
|
|
||||||
user_item = User(
|
|
||||||
name=user["name"],
|
|
||||||
card=user["card"],
|
|
||||||
rfid=user["rfid"],
|
|
||||||
credit=user["credit"],
|
|
||||||
)
|
|
||||||
user_items.append(user_item)
|
|
||||||
|
|
||||||
sql_session.add_all(product_items)
|
|
||||||
sql_session.add_all(user_items)
|
|
||||||
sql_session.commit()
|
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from dibbler.db import Session
|
||||||
|
|
||||||
from dibbler.models import User
|
from dibbler.models import User
|
||||||
|
|
||||||
|
|
||||||
def main(sql_session: Session) -> None:
|
def main():
|
||||||
|
# Start an SQL session
|
||||||
|
session = Session()
|
||||||
# Let's find all users with a negative credit
|
# Let's find all users with a negative credit
|
||||||
slabbedasker = sql_session.query(User).filter(User.credit < 0).all()
|
slabbedasker = session.query(User).filter(User.credit < 0).all()
|
||||||
|
|
||||||
for slubbert in slabbedasker:
|
for slubbert in slabbedasker:
|
||||||
print(f"{slubbert.name}, {slubbert.credit}")
|
print(f"{slubbert.name}, {slubbert.credit}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
+199
-199
@@ -1,231 +1,231 @@
|
|||||||
# #! /usr/bin/env python
|
#! /usr/bin/env python
|
||||||
|
|
||||||
# # TODO: fixme
|
# TODO: fixme
|
||||||
|
|
||||||
# # -*- coding: UTF-8 -*-
|
# -*- 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()
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[general]
|
||||||
|
; quit_allowed = false
|
||||||
|
; stop_allowed = false
|
||||||
|
quit_allowed = true ; not for prod
|
||||||
|
stop_allowed = true ; not for prod
|
||||||
|
show_tracebacks = true
|
||||||
|
input_encoding = 'utf8'
|
||||||
|
|
||||||
|
[database]
|
||||||
|
; url = postgresql://dibbler:hunter2@127.0.0.1/pvvvv
|
||||||
|
url = sqlite:///test.db ; devenv will override this to postgres using DIBBLER_DATABASE_URL
|
||||||
|
|
||||||
|
[limits]
|
||||||
|
low_credit_warning_limit = -100
|
||||||
|
user_recent_transaction_limit = 100
|
||||||
|
|
||||||
|
# See https://pypi.org/project/brother_ql_next/ for label types
|
||||||
|
# Set rotate to False for endless labels
|
||||||
|
[printer]
|
||||||
|
label_type = "62"
|
||||||
|
label_rotate = false
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
[general]
|
|
||||||
quit_allowed = true
|
|
||||||
stop_allowed = false
|
|
||||||
show_tracebacks = true
|
|
||||||
input_encoding = 'utf8'
|
|
||||||
|
|
||||||
[database]
|
|
||||||
type = 'sqlite'
|
|
||||||
|
|
||||||
[database.sqlite]
|
|
||||||
path = 'test.db'
|
|
||||||
|
|
||||||
[database.postgresql]
|
|
||||||
host = 'localhost'
|
|
||||||
# host = '/run/postgresql'
|
|
||||||
port = 5432
|
|
||||||
|
|
||||||
username = 'dibbler'
|
|
||||||
dbname = 'dibbler'
|
|
||||||
|
|
||||||
# You can either specify a path to a file containing the password,
|
|
||||||
# or just specify the password directly
|
|
||||||
# password = 'superhemlig'
|
|
||||||
# password_file = '/var/lib/dibbler/db-password'
|
|
||||||
|
|
||||||
|
|
||||||
[limits]
|
|
||||||
low_credit_warning_limit = -100
|
|
||||||
user_recent_transaction_limit = 100
|
|
||||||
|
|
||||||
# See https://pypi.org/project/brother_ql/ for label types
|
|
||||||
# Set rotate to False for endless labels
|
|
||||||
[printer]
|
|
||||||
label_type = '62'
|
|
||||||
label_rotate = false
|
|
||||||
Generated
+274
-4
@@ -1,12 +1,217 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"cachix": {
|
||||||
|
"inputs": {
|
||||||
|
"devenv": [
|
||||||
|
"devenv"
|
||||||
|
],
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv"
|
||||||
|
],
|
||||||
|
"git-hooks": [
|
||||||
|
"devenv"
|
||||||
|
],
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1728672398,
|
||||||
|
"narHash": "sha256-KxuGSoVUFnQLB2ZcYODW7AVPAh9JqRlD5BrfsC/Q4qs=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "cachix",
|
||||||
|
"rev": "aac51f698309fd0f381149214b7eee213c66ef0a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"ref": "latest",
|
||||||
|
"repo": "cachix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devenv": {
|
||||||
|
"inputs": {
|
||||||
|
"cachix": "cachix",
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"git-hooks": "git-hooks",
|
||||||
|
"nix": "nix",
|
||||||
|
"nixpkgs": "nixpkgs_3"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731619804,
|
||||||
|
"narHash": "sha256-wyxFaVooL8SzvQNpolpx32X+GoBPnCAg9E0i/Ekn3FU=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"rev": "87edaaf1dddf17fe16eabab3c8edaf7cca2c3bc2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696426674,
|
||||||
|
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": [
|
||||||
|
"devenv",
|
||||||
|
"nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1712014858,
|
||||||
|
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv"
|
||||||
|
],
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"nixpkgs-stable": [
|
||||||
|
"devenv"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1730302582,
|
||||||
|
"narHash": "sha256-W1MIJpADXQCgosJZT8qBYLRuZls2KSiKdpnTVdKBuvU=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"devenv",
|
||||||
|
"git-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libgit2": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1697646580,
|
||||||
|
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
|
||||||
|
"owner": "libgit2",
|
||||||
|
"repo": "libgit2",
|
||||||
|
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "libgit2",
|
||||||
|
"repo": "libgit2",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": [
|
||||||
|
"devenv"
|
||||||
|
],
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"libgit2": "libgit2",
|
||||||
|
"nixpkgs": "nixpkgs_2",
|
||||||
|
"nixpkgs-23-11": [
|
||||||
|
"devenv"
|
||||||
|
],
|
||||||
|
"nixpkgs-regression": [
|
||||||
|
"devenv"
|
||||||
|
],
|
||||||
|
"pre-commit-hooks": [
|
||||||
|
"devenv"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1727438425,
|
||||||
|
"narHash": "sha256-X8ES7I1cfNhR9oKp06F6ir4Np70WGZU5sfCOuNBEwMg=",
|
||||||
|
"owner": "domenkozar",
|
||||||
|
"repo": "nix",
|
||||||
|
"rev": "f6c5ae4c1b2e411e6b1e6a8181cc84363d6a7546",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "domenkozar",
|
||||||
|
"ref": "devenv-2.24",
|
||||||
|
"repo": "nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1779560665,
|
"lastModified": 1730531603,
|
||||||
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
"narHash": "sha256-Dqg6si5CqIzm87sp57j5nTaeBbWhHFaVyG7V6L8k3lY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
"rev": "7ffd9ae656aec493492b44d0ddfb28e79a1ea25d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -16,9 +221,74 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1717432640,
|
||||||
|
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "release-24.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_3": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1716977621,
|
||||||
|
"narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv-nixpkgs",
|
||||||
|
"rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"ref": "rolling",
|
||||||
|
"repo": "devenv-nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_4": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731611831,
|
||||||
|
"narHash": "sha256-R51rOqkWMfubBkZ9BY4Y1VaRoeqEBshlfQ8mMH5RjqI=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "cea28c811faadb50bee00d433bbf2fea845a43e4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable-small",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"devenv": "devenv",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs_4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,73 +1,128 @@
|
|||||||
{
|
{
|
||||||
description = "Dibbler samspleisebod";
|
description = "Dibbler samspleisebod";
|
||||||
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable-small";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
devenv.url = "github:cachix/devenv";
|
||||||
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs }: let
|
nixConfig = {
|
||||||
inherit (nixpkgs) lib;
|
extra-trusted-public-keys = [
|
||||||
|
"devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="
|
||||||
systems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
];
|
||||||
|
extra-substituters = [
|
||||||
|
"https://devenv.cachix.org"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
forAllSystems = f: lib.genAttrs systems (system: let
|
outputs = { self, ... } @ inputs:
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
inputs.flake-utils.lib.eachDefaultSystem (system: let
|
||||||
in f system pkgs);
|
pkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||||
in {
|
inherit (pkgs) lib;
|
||||||
apps = let
|
in {
|
||||||
mkApp = program: description: {
|
|
||||||
type = "app";
|
packages = {
|
||||||
program = toString program;
|
default = self.packages.${system}.dibbler;
|
||||||
meta = {
|
|
||||||
inherit description;
|
dibbler = pkgs.python311Packages.callPackage ./nix/dibbler.nix { };
|
||||||
};
|
skrot-vm = self.nixosConfigurations.skrot.config.system.build.vm;
|
||||||
};
|
|
||||||
mkVm = name: mkApp "${self.nixosConfigurations.${name}.config.system.build.vm}/bin/run-nixos-vm";
|
# devenv cruft
|
||||||
in forAllSystems (system: pkgs: {
|
devenv-up = self.devShells.${system}.default.config.procfileScript;
|
||||||
default = self.apps.${system}.dibbler;
|
devenv-test = self.devShells.${system}.default.config.test;
|
||||||
dibbler = let
|
};
|
||||||
app = pkgs.writeShellApplication {
|
|
||||||
name = "dibbler-with-default-config";
|
devShells = {
|
||||||
runtimeInputs = [ self.packages.${system}.dibbler ];
|
default = self.devShells.${system}.dibbler;
|
||||||
text = ''
|
dibbler = inputs.devenv.lib.mkShell {
|
||||||
dibbler -c ${./example-config.toml} "$@"
|
inherit inputs pkgs;
|
||||||
|
modules = [({ config, ... }: {
|
||||||
|
# https://devenv.sh/reference/options/
|
||||||
|
|
||||||
|
enterShell = ''
|
||||||
|
if [[ ! -f config.ini ]]; then
|
||||||
|
cp -v example-config.ini config.ini
|
||||||
|
fi
|
||||||
|
|
||||||
|
export REPO_ROOT=$(realpath .) # used by mkPythonEditablePackage
|
||||||
|
export DIBBLER_CONFIG_FILE=$(realpath config.ini)
|
||||||
|
export DIBBLER_DATABASE_URL=postgresql://dibbler:hunter2@/dibbler?host=${config.env.PGHOST}
|
||||||
'';
|
'';
|
||||||
};
|
|
||||||
in mkApp (lib.getExe app) "Run the dibbler cli with its default config against an SQLite database";
|
|
||||||
vm = mkVm "vm" "Start a NixOS VM with dibbler installed in kiosk-mode";
|
|
||||||
vm-non-kiosk = mkVm "vm-non-kiosk" "Start a NixOS VM with dibbler installed in nonkiosk-mode";
|
|
||||||
});
|
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
|
||||||
|
/* self.packages.${system}.dibbler */
|
||||||
|
(pkgs.python311Packages.mkPythonEditablePackage {
|
||||||
|
inherit (self.packages.${system}.dibbler)
|
||||||
|
pname version
|
||||||
|
build-system dependencies;
|
||||||
|
scripts = (lib.importTOML ./pyproject.toml).project.scripts;
|
||||||
|
root = "$REPO_ROOT";
|
||||||
|
})
|
||||||
|
|
||||||
|
pkgs.python311Packages.black
|
||||||
|
pkgs.ruff
|
||||||
|
];
|
||||||
|
|
||||||
|
services.postgres = {
|
||||||
|
enable = true;
|
||||||
|
initialDatabases = [
|
||||||
|
{
|
||||||
|
name = "dibbler";
|
||||||
|
user = "dibbler";
|
||||||
|
pass = "hunter2";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
})];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
{
|
||||||
|
# Note: using the module requires that you have applied the
|
||||||
|
# overlay first
|
||||||
nixosModules.default = import ./nix/module.nix;
|
nixosModules.default = import ./nix/module.nix;
|
||||||
|
|
||||||
nixosConfigurations = {
|
images.skrot = self.nixosConfigurations.skrot.config.system.build.sdImage;
|
||||||
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 = inputs.nixpkgs.lib.nixosSystem {
|
||||||
|
system = "aarch64-linux";
|
||||||
|
modules = [
|
||||||
|
(inputs.nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix")
|
||||||
|
self.nixosModules.default
|
||||||
|
({...}: {
|
||||||
|
system.stateVersion = "22.05";
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
hostName = "skrot";
|
||||||
|
domain = "pvv.ntnu.no";
|
||||||
|
nameservers = [ "129.241.0.200" "129.241.0.201" ];
|
||||||
|
defaultGateway = "129.241.210.129";
|
||||||
|
interfaces.eth0 = {
|
||||||
|
useDHCP = false;
|
||||||
|
ipv4.addresses = [{
|
||||||
|
address = "129.241.210.235";
|
||||||
|
prefixLength = 25;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# services.resolved.enable = true;
|
||||||
|
# systemd.network.enable = true;
|
||||||
|
# systemd.network.networks."30-network" = {
|
||||||
|
# matchConfig.Name = "*";
|
||||||
|
# DHCP = "no";
|
||||||
|
# address = [ "129.241.210.235/25" ];
|
||||||
|
# gateway = [ "129.241.210.129" ];
|
||||||
|
# };
|
||||||
|
})
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
overlays = {
|
|
||||||
default = self.overlays.dibbler;
|
|
||||||
dibbler = final: prev: {
|
|
||||||
inherit (self.packages.${prev.stdenv.hostPlatform.system}) dibbler;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
devShells = forAllSystems (system: pkgs: {
|
|
||||||
default = self.devShells.${system}.dibbler;
|
|
||||||
dibbler = pkgs.callPackage ./nix/shell.nix {
|
|
||||||
python = pkgs.python313;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
packages = forAllSystems (system: pkgs: {
|
|
||||||
default = self.packages.${system}.dibbler;
|
|
||||||
dibbler = pkgs.callPackage ./nix/package.nix {
|
|
||||||
python3Packages = pkgs.python313Packages;
|
|
||||||
inherit (self) sourceInfo;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
{
|
|
||||||
"products": [
|
|
||||||
{
|
|
||||||
"product_id": 1,
|
|
||||||
"bar_code": "1234567890123",
|
|
||||||
"name": "Wireless Mouse",
|
|
||||||
"price": 2999,
|
|
||||||
"stock": 150,
|
|
||||||
"hidden": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"product_id": 2,
|
|
||||||
"bar_code": "9876543210987",
|
|
||||||
"name": "Mechanical Keyboard",
|
|
||||||
"price": 5999,
|
|
||||||
"stock": 75,
|
|
||||||
"hidden": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"product_id": 3,
|
|
||||||
"bar_code": "1112223334445",
|
|
||||||
"name": "Gaming Monitor",
|
|
||||||
"price": 19999,
|
|
||||||
"stock": 20,
|
|
||||||
"hidden": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"product_id": 4,
|
|
||||||
"bar_code": "5556667778889",
|
|
||||||
"name": "USB-C Docking Station",
|
|
||||||
"price": 8999,
|
|
||||||
"stock": 50,
|
|
||||||
"hidden": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"product_id": 5,
|
|
||||||
"bar_code": "4445556667771",
|
|
||||||
"name": "Noise Cancelling Headphones",
|
|
||||||
"price": 12999,
|
|
||||||
"stock": 30,
|
|
||||||
"hidden": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"name": "Albert",
|
|
||||||
"credit": 42069,
|
|
||||||
"card": "NTU12345678",
|
|
||||||
"rfid": "a1b2c3d4e5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "lorem",
|
|
||||||
"credit": 2000,
|
|
||||||
"card": "9876543210",
|
|
||||||
"rfid": "f6e7d8c9b0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ibsum",
|
|
||||||
"credit": 1000,
|
|
||||||
"card": "11122233",
|
|
||||||
"rfid": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dave",
|
|
||||||
"credit": 7500,
|
|
||||||
"card": "NTU56789012",
|
|
||||||
"rfid": "1234abcd5678"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "eve",
|
|
||||||
"credit": 3000,
|
|
||||||
"card": null,
|
|
||||||
"rfid": "deadbeef1234"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{ lib
|
||||||
|
, fetchFromGitHub
|
||||||
|
, buildPythonApplication
|
||||||
|
, setuptools
|
||||||
|
, brother-ql
|
||||||
|
, matplotlib
|
||||||
|
, psycopg2
|
||||||
|
, python-barcode
|
||||||
|
, sqlalchemy
|
||||||
|
}:
|
||||||
|
|
||||||
|
buildPythonApplication {
|
||||||
|
pname = "dibbler";
|
||||||
|
version = "0.0.0";
|
||||||
|
pyproject = true;
|
||||||
|
|
||||||
|
src = lib.cleanSource ../.;
|
||||||
|
|
||||||
|
build-system = [ setuptools ];
|
||||||
|
dependencies = [
|
||||||
|
# we override pname to satisfy mkPythonEditablePackage
|
||||||
|
(brother-ql.overridePythonAttrs { pname = "brother-ql-next"; })
|
||||||
|
matplotlib
|
||||||
|
psycopg2
|
||||||
|
python-barcode
|
||||||
|
sqlalchemy
|
||||||
|
];
|
||||||
|
}
|
||||||
+71
-170
@@ -1,186 +1,87 @@
|
|||||||
{ config, pkgs, lib, ... }: let
|
{ config, pkgs, lib, ... }: let
|
||||||
cfg = config.services.dibbler;
|
cfg = config.services.dibbler;
|
||||||
|
|
||||||
format = pkgs.formats.toml { };
|
|
||||||
in {
|
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 {
|
||||||
screenPackage = lib.mkPackageOption pkgs "screen" { };
|
default = ../conf.py;
|
||||||
|
|
||||||
createLocalDatabase = lib.mkEnableOption "" // {
|
|
||||||
description = ''
|
|
||||||
Whether to set up a local postgres database automatically.
|
|
||||||
|
|
||||||
::: {.note}
|
|
||||||
You must set up postgres manually before enabling this option.
|
|
||||||
:::
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
kioskMode = lib.mkEnableOption "" // {
|
|
||||||
description = ''
|
|
||||||
Whether to let dibbler take over the entire machine.
|
|
||||||
|
|
||||||
This will restrict the machine to a single TTY and make the program unquittable.
|
|
||||||
You can still get access to PTYs via SSH and similar, if enabled.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
limitScreenHeight = lib.mkOption {
|
|
||||||
type = with lib.types; nullOr ints.unsigned;
|
|
||||||
default = null;
|
|
||||||
example = 42;
|
|
||||||
description = ''
|
|
||||||
If set, limits the height of the screen dibbler uses to the given number of lines.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
limitScreenWidth = lib.mkOption {
|
|
||||||
type = with lib.types; nullOr ints.unsigned;
|
|
||||||
default = null;
|
|
||||||
example = 80;
|
|
||||||
description = ''
|
|
||||||
If set, limits the width of the screen dibbler uses to the given number of columns.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = lib.mkOption {
|
|
||||||
description = "Configuration for dibbler";
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
freeformType = format.type;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
config = let
|
||||||
{
|
screen = "${pkgs.screen}/bin/screen";
|
||||||
services.dibbler.settings = lib.pipe ../example-config.toml [
|
in {
|
||||||
builtins.readFile
|
boot = {
|
||||||
builtins.fromTOML
|
consoleLogLevel = 0;
|
||||||
(lib.mapAttrsRecursive (_: lib.mkDefault))
|
enableContainers = false;
|
||||||
];
|
loader.grub.enable = false;
|
||||||
}
|
};
|
||||||
{
|
|
||||||
environment.systemPackages = [ cfg.package ];
|
|
||||||
|
|
||||||
environment.etc."dibbler/dibbler.toml".source = format.generate "dibbler.toml" cfg.settings;
|
users = {
|
||||||
|
groups.dibbler = { };
|
||||||
users = {
|
users.dibbler = {
|
||||||
users.dibbler = {
|
group = "dibbler";
|
||||||
group = "dibbler";
|
|
||||||
isNormalUser = true;
|
|
||||||
};
|
|
||||||
groups.dibbler = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
services.dibbler.settings.database = lib.mkIf cfg.createLocalDatabase {
|
|
||||||
type = "postgresql";
|
|
||||||
postgresql.host = "/run/postgresql";
|
|
||||||
};
|
|
||||||
|
|
||||||
services.postgresql = lib.mkIf cfg.createLocalDatabase {
|
|
||||||
ensureDatabases = [ "dibbler" ];
|
|
||||||
ensureUsers = [{
|
|
||||||
name = "dibbler";
|
|
||||||
ensureDBOwnership = true;
|
|
||||||
ensureClauses.login = true;
|
|
||||||
}];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.dibbler-setup-database = lib.mkIf cfg.createLocalDatabase {
|
|
||||||
description = "Dibbler database setup";
|
|
||||||
wantedBy = [ "default.target" ];
|
|
||||||
after = [ "postgresql.service" ];
|
|
||||||
unitConfig = {
|
|
||||||
ConditionPathExists = "!/var/lib/dibbler/.db-setup-done";
|
|
||||||
};
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
ExecStart = "${lib.getExe cfg.package} --config /etc/dibbler/dibbler.toml create-db";
|
|
||||||
ExecStartPost = "${lib.getExe' pkgs.coreutils "touch"} /var/lib/dibbler/.db-setup-done";
|
|
||||||
StateDirectory = "dibbler";
|
|
||||||
|
|
||||||
User = "dibbler";
|
|
||||||
Group = "dibbler";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(lib.mkIf cfg.kioskMode {
|
|
||||||
boot.kernelParams = [
|
|
||||||
"console=tty1"
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
users.users.dibbler = {
|
|
||||||
extraGroups = [ "lp" ];
|
extraGroups = [ "lp" ];
|
||||||
shell = (pkgs.writeShellScriptBin "login-shell" "${lib.getExe' cfg.screenPackage "screen"} -x dibbler") // {
|
isNormalUser = true;
|
||||||
shellPath = "/bin/login-shell";
|
shell = (
|
||||||
};
|
(pkgs.writeShellScriptBin "login-shell" "${screen} -x dibbler")
|
||||||
|
// {shellPath = "/bin/login-shell";}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.screen-daemon = {
|
||||||
|
description = "Dibbler service screen";
|
||||||
|
wantedBy = [ "default.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStartPre = "-${screen} -X -S dibbler kill";
|
||||||
|
ExecStart = "${screen} -dmS dibbler -O -l ${cfg.package}/bin/dibbler --config ${cfg.config} loop";
|
||||||
|
ExecStartPost = "${screen} -X -S dibbler width 42 80";
|
||||||
|
User = "dibbler";
|
||||||
|
Group = "dibbler";
|
||||||
|
Type = "forking";
|
||||||
|
RemainAfterExit = false;
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "5s";
|
||||||
|
SuccessExitStatus = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# https://github.com/NixOS/nixpkgs/issues/84105
|
||||||
|
boot.kernelParams = [
|
||||||
|
"console=ttyUSB0,9600"
|
||||||
|
"console=tty1"
|
||||||
|
];
|
||||||
|
systemd.services."serial-getty@ttyUSB0" = {
|
||||||
|
enable = true;
|
||||||
|
wantedBy = [ "getty.target" ]; # to start at boot
|
||||||
|
serviceConfig.Restart = "always"; # restart when session is closed
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
openssh = {
|
||||||
|
enable = true;
|
||||||
|
permitRootLogin = "yes";
|
||||||
};
|
};
|
||||||
|
|
||||||
services.dibbler.settings.general = {
|
getty.autologinUser = lib.mkForce "dibbler";
|
||||||
quit_allowed = false;
|
udisks2.enable = false;
|
||||||
stop_allowed = false;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.dibbler-screen-session = {
|
networking.firewall.logRefusedConnections = false;
|
||||||
description = "Dibbler Screen Session";
|
console.keyMap = "no";
|
||||||
wantedBy = [
|
programs.command-not-found.enable = false;
|
||||||
"default.target"
|
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
|
||||||
];
|
environment.noXlibs = true;
|
||||||
after = if cfg.createLocalDatabase then [
|
|
||||||
"postgresql.service"
|
|
||||||
"dibbler-setup-database.service"
|
|
||||||
] else [
|
|
||||||
"network.target"
|
|
||||||
];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "forking";
|
|
||||||
RemainAfterExit = false;
|
|
||||||
Restart = "always";
|
|
||||||
RestartSec = "5s";
|
|
||||||
SuccessExitStatus = 1;
|
|
||||||
|
|
||||||
User = "dibbler";
|
documentation = {
|
||||||
Group = "dibbler";
|
info.enable = false;
|
||||||
|
man.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
ExecStartPre = "-${lib.getExe' cfg.screenPackage "screen"} -X -S dibbler kill";
|
security = {
|
||||||
ExecStart = let
|
polkit.enable = lib.mkForce false;
|
||||||
screenArgs = lib.escapeShellArgs [
|
audit.enable = false;
|
||||||
# -dm creates the screen in detached mode without accessing it
|
};
|
||||||
"-dm"
|
};
|
||||||
|
|
||||||
# Session name
|
|
||||||
"-S"
|
|
||||||
"dibbler"
|
|
||||||
|
|
||||||
# Set optimal output mode instead of VT100 emulation
|
|
||||||
"-O"
|
|
||||||
|
|
||||||
# Enable login mode, updates utmp entries
|
|
||||||
"-l"
|
|
||||||
];
|
|
||||||
|
|
||||||
dibblerArgs = lib.cli.toCommandLineShellGNU { } {
|
|
||||||
config = "/etc/dibbler/dibbler.toml";
|
|
||||||
};
|
|
||||||
|
|
||||||
in "${lib.getExe' cfg.screenPackage "screen"} ${screenArgs} ${lib.getExe cfg.package} ${dibblerArgs} loop";
|
|
||||||
ExecStartPost =
|
|
||||||
lib.optionals (cfg.limitScreenWidth != null) [
|
|
||||||
"${lib.getExe' cfg.screenPackage "screen"} -X -S dibbler width ${toString cfg.limitScreenWidth}"
|
|
||||||
]
|
|
||||||
++ lib.optionals (cfg.limitScreenHeight != null) [
|
|
||||||
"${lib.getExe' cfg.screenPackage "screen"} -X -S dibbler height ${toString cfg.limitScreenHeight}"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
services.getty.autologinUser = "dibbler";
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
{ self, nixpkgs, ... }:
|
|
||||||
nixpkgs.lib.nixosSystem {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
overlays = [
|
|
||||||
self.overlays.dibbler
|
|
||||||
];
|
|
||||||
};
|
|
||||||
modules = [
|
|
||||||
"${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix"
|
|
||||||
"${nixpkgs}/nixos/tests/common/user-account.nix"
|
|
||||||
|
|
||||||
self.nixosModules.default
|
|
||||||
|
|
||||||
({ config, ... }: {
|
|
||||||
system.stateVersion = config.system.nixos.release;
|
|
||||||
virtualisation.graphics = false;
|
|
||||||
|
|
||||||
users.motd = ''
|
|
||||||
=================================
|
|
||||||
Welcome to the dibbler non-kiosk vm!
|
|
||||||
|
|
||||||
Try running:
|
|
||||||
${config.services.dibbler.package.meta.mainProgram} loop
|
|
||||||
|
|
||||||
Password for dibbler is 'dibbler'
|
|
||||||
|
|
||||||
To exit, press Ctrl+A, then X
|
|
||||||
=================================
|
|
||||||
'';
|
|
||||||
|
|
||||||
users.users.dibbler = {
|
|
||||||
isNormalUser = true;
|
|
||||||
password = "dibbler";
|
|
||||||
extraGroups = [ "wheel" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.getty.autologinUser = "dibbler";
|
|
||||||
|
|
||||||
programs.vim = {
|
|
||||||
enable = true;
|
|
||||||
defaultEditor = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
services.postgresql.enable = true;
|
|
||||||
|
|
||||||
services.dibbler = {
|
|
||||||
enable = true;
|
|
||||||
createLocalDatabase = true;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{ self, nixpkgs, ... }:
|
|
||||||
nixpkgs.lib.nixosSystem {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
overlays = [
|
|
||||||
self.overlays.default
|
|
||||||
];
|
|
||||||
};
|
|
||||||
modules = [
|
|
||||||
"${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix"
|
|
||||||
"${nixpkgs}/nixos/tests/common/user-account.nix"
|
|
||||||
|
|
||||||
self.nixosModules.default
|
|
||||||
|
|
||||||
({ config, ... }: {
|
|
||||||
system.stateVersion = config.system.nixos.release;
|
|
||||||
virtualisation.graphics = false;
|
|
||||||
|
|
||||||
services.postgresql.enable = true;
|
|
||||||
|
|
||||||
services.dibbler = {
|
|
||||||
enable = true;
|
|
||||||
createLocalDatabase = true;
|
|
||||||
kioskMode = true;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
{ lib
|
|
||||||
, sourceInfo
|
|
||||||
, python3Packages
|
|
||||||
, makeWrapper
|
|
||||||
, less
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
pyproject = builtins.fromTOML (builtins.readFile ../pyproject.toml);
|
|
||||||
in
|
|
||||||
python3Packages.buildPythonApplication {
|
|
||||||
pname = pyproject.project.name;
|
|
||||||
version = "0.1";
|
|
||||||
src = lib.cleanSource ../.;
|
|
||||||
|
|
||||||
format = "pyproject";
|
|
||||||
|
|
||||||
# brother-ql is breaky breaky
|
|
||||||
# https://github.com/NixOS/nixpkgs/issues/285234
|
|
||||||
# dontCheckRuntimeDeps = true;
|
|
||||||
|
|
||||||
env.SETUPTOOLS_SCM_PRETEND_METADATA = (x: "{${x}}") (lib.concatStringsSep ", " [
|
|
||||||
"node=\"${sourceInfo.rev or (lib.substring 0 64 sourceInfo.dirtyRev)}\""
|
|
||||||
"node_date=${lib.substring 0 4 sourceInfo.lastModifiedDate}-${lib.substring 4 2 sourceInfo.lastModifiedDate}-${lib.substring 6 2 sourceInfo.lastModifiedDate}"
|
|
||||||
"dirty=${if sourceInfo ? dirtyRev then "true" else "false"}"
|
|
||||||
]);
|
|
||||||
|
|
||||||
nativeBuildInputs = with python3Packages; [
|
|
||||||
makeWrapper
|
|
||||||
setuptools
|
|
||||||
setuptools-scm
|
|
||||||
];
|
|
||||||
propagatedBuildInputs = with python3Packages; [
|
|
||||||
# brother-ql
|
|
||||||
# matplotlib
|
|
||||||
psycopg2-binary
|
|
||||||
# python-barcode
|
|
||||||
sqlalchemy
|
|
||||||
];
|
|
||||||
|
|
||||||
postInstall = ''
|
|
||||||
wrapProgram $out/bin/dibbler \
|
|
||||||
--prefix PATH : "${lib.makeBinPath [ less ]}"
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "The little kiosk that could";
|
|
||||||
mainProgram = "dibbler";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
mkShell,
|
|
||||||
python,
|
|
||||||
ruff,
|
|
||||||
uv,
|
|
||||||
}:
|
|
||||||
|
|
||||||
mkShell {
|
|
||||||
packages = [
|
|
||||||
ruff
|
|
||||||
uv
|
|
||||||
(python.withPackages (ps: with ps; [
|
|
||||||
# brother-ql
|
|
||||||
# matplotlib
|
|
||||||
psycopg2
|
|
||||||
# python-barcode
|
|
||||||
sqlalchemy
|
|
||||||
]))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
+10
-47
@@ -1,71 +1,34 @@
|
|||||||
[build-system]
|
[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"
|
||||||
dynamic = ["version"]
|
authors = []
|
||||||
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_next",
|
||||||
# "matplotlib",
|
"matplotlib",
|
||||||
"psycopg2-binary >= 2.8, <2.10",
|
"psycopg2 >= 2.8, <2.10",
|
||||||
# "python-barcode",
|
"python-barcode",
|
||||||
]
|
]
|
||||||
scripts.dibbler = "dibbler.main:main"
|
dynamic = ["version"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["dibbler*"]
|
include = ["dibbler*"]
|
||||||
|
|
||||||
[tool.setuptools_scm]
|
[project.scripts]
|
||||||
version_file = "dibbler/_version.py"
|
dibbler = "dibbler.main:main"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
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
|
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 3
|
|
||||||
requires-python = ">=3.11"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dibbler"
|
|
||||||
source = { editable = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "psycopg2-binary" },
|
|
||||||
{ name = "sqlalchemy" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.8,<2.10" },
|
|
||||||
{ name = "sqlalchemy", specifier = ">=2.0,<2.1" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "greenlet"
|
|
||||||
version = "3.5.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", size = 284764, upload-time = "2026-05-20T13:09:10.204Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", size = 603479, upload-time = "2026-05-20T14:00:04.757Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", size = 615495, upload-time = "2026-05-20T14:05:40.87Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", size = 614754, upload-time = "2026-05-20T13:14:24.947Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", size = 1574097, upload-time = "2026-05-20T14:02:24.003Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", size = 1641058, upload-time = "2026-05-20T13:14:31.83Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/56/5f332b7705545eac2dc01b4e9254d24a793f2656d55d5cc6b94ee59d22ae/greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e", size = 238089, upload-time = "2026-05-20T13:14:03.229Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/a9/a3c2fa886c5b94863fb0e61b3bc14610b7aa94cf4f17f8741b11708305fc/greenlet-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:cc6ab7e555c8a112ad3a76e368e86e12a2754bcae1652a5602e133ec7b635523", size = 234989, upload-time = "2026-05-20T13:08:27.715Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580, upload-time = "2026-05-20T13:08:45.056Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "psycopg2-binary"
|
|
||||||
version = "2.9.12"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/19/d4ce60954f3bb9d8e3bc5e5c4d1f2487de2d3851bf2391d54954c9df12a6/psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", size = 3712338, upload-time = "2026-04-20T23:34:03.961Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/71/c85409ee0d78890f0660eff262e815e7dd2bb741a17611d82e9e8cd9dc5e/psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", size = 3822407, upload-time = "2026-04-20T23:34:05.977Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/ed/60486c2c7f0d4d1ede2bfb1ed27e2498477ce646bc7f6b2759906303117e/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", size = 4578425, upload-time = "2026-04-20T23:34:08.246Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/b9/656cb03fad9f4f49f2145c334b1126ee75189929ca4e6187d485a2d59951/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", size = 4273709, upload-time = "2026-04-20T23:34:10.974Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/66/08cf0da0e25cc6fb142c89be45fc8418792858f0c4cbff5e24530ff02cd6/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", size = 5893779, upload-time = "2026-04-20T23:34:13.905Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/d7/eecd9ce8e146d3721115d82d3836efdbb712187e4590325df549989d18f4/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", size = 4109308, upload-time = "2026-04-20T23:34:16.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/2e/b1dc289b362cc8d45697b57eefbd673186f49a4ea0906928988e3affcc98/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", size = 3654405, upload-time = "2026-04-20T23:34:19.303Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/e4/4c4aea6473214dbdbd0fbba11aa4691e76dc01722c55724c5951719865ff/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", size = 3299187, upload-time = "2026-04-20T23:34:21.206Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/5d/b03b99986446a4f57b170ed9a2579fb7ff9783ca0fa5226b19db99737fee/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", size = 3047716, upload-time = "2026-04-20T23:34:23.077Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/86/382ee4afbd1d97500c9d2862b20c2fdeddf4b7335e984df3fb4309f64108/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", size = 3349237, upload-time = "2026-04-20T23:34:25.211Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/16/9a57c75ba1eda7165c017342f526810d5f5a12647dde749c99ae9a7141d7/psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", size = 2757036, upload-time = "2026-04-20T23:34:27.77Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sqlalchemy"
|
|
||||||
version = "2.0.50"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/5d/3172686af1770e4de2805f919a51441085f589ddadf3dd76ec582f84f497/sqlalchemy-2.0.50-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa6e403663a9c43c8fef7ce4bdb4cf48bcd8d352e91deda2a99f963270bd508", size = 2161366, upload-time = "2026-05-24T20:00:02.061Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/90/e98dedea3c3e663a17afcd003a34ba45efdac2cea3b6f2e4585e2b1e2537/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51b637a84f9fa35ae1f9017e786cb142974a25305085e1b378b3647a67f65ad3", size = 3318926, upload-time = "2026-05-24T20:07:42.369Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/4f/501308c2babb62c11753ecb4ee88ba9eef019419a4d6cbf7cb13e2bad353/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dab927761d9108550f0cf8e66ff21af56f907a0ce0a689793db615e2b55f62c", size = 3319199, upload-time = "2026-05-24T20:14:28.551Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/39/d88996c5e03ed6248c3a788d20f0b8d8b376b9f8a495e4bab9df7c72d2f8/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:545eae198d37bcf837a10ede3684e2af32458d6f35c597c35c2de7502dc38fc4", size = 3270301, upload-time = "2026-05-24T20:07:44.917Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/1b/1ae0e65161b51cc43e5ca75430ef79d80e23b5042d645586c2c342c3b92e/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fec460e18cdbb4c7773531122ce9a27e96c6ca17af3933941d94da475ad2c86", size = 3293465, upload-time = "2026-05-24T20:14:30.501Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/29/17c0003f2c0dfa6d1b97672475707e3ec5980db09defd7fa20beb6833bbd/sqlalchemy-2.0.50-cp311-cp311-win32.whl", hash = "sha256:e6e814658818fd165e749e3d8490ef16cc7f379a118c37ada8b0589ffbaaac22", size = 2120694, upload-time = "2026-05-24T20:08:09.237Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/18/280d00654cc19d1fccf236fa5070f6dd04b84dde6f1b2e637bde0ff340a7/sqlalchemy-2.0.50-cp311-cp311-win_amd64.whl", hash = "sha256:1c5f858fe79c9f5d8fda065c06186356acb7f8df3cd52dbd5ee3f200e4b144f5", size = 2145315, upload-time = "2026-05-24T20:08:10.952Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/b0/a9d19b43f38f878b1278bca5b00b909f7540d41494396dd2561f9ad0956d/sqlalchemy-2.0.50-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb", size = 2159807, upload-time = "2026-05-24T19:27:53.086Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/2c/191dd58a248fd2cfd4780fa82c375c505e4ad98c8b522fa69ec492130d77/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89", size = 3343358, upload-time = "2026-05-24T20:09:29.279Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/2b/514fce8a7df81cf5bad7ff7865de7ac0c5776a38cc043475c4703eb7fe8b/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600", size = 3357994, upload-time = "2026-05-24T20:17:13.495Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/a6/a0e283f5494f92b0d77e319ff77e437b1ffe4a051ba67c81d53234825475/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e", size = 3289399, upload-time = "2026-05-24T20:09:32.239Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/96/1b07325ba71752d6a028b77d07bed1483ad545f794e8b1dc89b3ba3b3c68/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615", size = 3321216, upload-time = "2026-05-24T20:17:15.581Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/8e/bad6ed253e8a99edfc99af02f7173ec48a1d3ed1b9b35a1b8bc1700900cc/sqlalchemy-2.0.50-cp312-cp312-win32.whl", hash = "sha256:1208050441471d003b7c8cb4054fb084f185cf35ac3f0ea270803865bca9939a", size = 2119194, upload-time = "2026-05-24T19:50:04.943Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/2d/314a6690dda4b9cfc571eab1a63cf6fe6e1470aa3759ccda6aa016ee0f5a/sqlalchemy-2.0.50-cp312-cp312-win_amd64.whl", hash = "sha256:9d1af51558029a156a70986b7df88f042b3d158d7c8d8fb5072912d4b32d89c7", size = 2146186, upload-time = "2026-05-24T19:50:06.74Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-extensions"
|
|
||||||
version = "4.15.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user