Compare commits
1 Commits
stable_dep
...
init-batch
| Author | SHA1 | Date | |
|---|---|---|---|
|
0463aff086
|
10
flake.nix
10
flake.nix
@@ -57,15 +57,7 @@
|
||||
mkVm = name: mkApp "${self.nixosConfigurations.${name}.config.system.build.vm}/bin/run-nixos-vm";
|
||||
in {
|
||||
default = self.apps.${system}.worblehat;
|
||||
worblehat = let
|
||||
app = pkgs.writeShellApplication {
|
||||
name = "worblehat-with-default-config";
|
||||
runtimeInputs = [ self.packages.${system}.worblehat ];
|
||||
text = ''
|
||||
worblehat -c ${./config-template.toml} "$@"
|
||||
'';
|
||||
};
|
||||
in mkApp (lib.getExe app) "Run the worblehat cli with its default config against an SQLite database";
|
||||
worblehat = mkApp (lib.getExe self.packages.${system}.worblehat) "Run worblehat without any setup";
|
||||
vm = mkVm "vm" "Start a NixOS VM with worblehat installed in kiosk-mode";
|
||||
vm-non-kiosk = mkVm "vm-non-kiosk" "Start a NixOS VM with worblehat installed in nonkiosk-mode";
|
||||
});
|
||||
|
||||
273
nix/module.nix
273
nix/module.nix
@@ -54,43 +54,10 @@ in {
|
||||
freeformType = format.type;
|
||||
};
|
||||
};
|
||||
|
||||
deadline-daemon = {
|
||||
enable = lib.mkEnableOption "" // {
|
||||
description = ''
|
||||
Whether to enable the worblehat deadline-daemon service,
|
||||
which periodically checks for upcoming deadlines and notifies users.
|
||||
|
||||
Note that this service is independent of the main worblehat service,
|
||||
and must be enabled separately.
|
||||
'';
|
||||
};
|
||||
|
||||
onCalendar = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
How often to trigger rendering the map,
|
||||
in the format of a systemd timer onCalendar configuration.
|
||||
|
||||
See {manpage}`systemd.timer(5)`.
|
||||
'';
|
||||
default = "*-*-* 10:15:00";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf (cfg.enable || cfg.deadline-daemon.enable) {
|
||||
environment.etc."worblehat/config.toml".source = format.generate "worblehat-config.toml" cfg.settings;
|
||||
|
||||
users = {
|
||||
users.worblehat = {
|
||||
group = "worblehat";
|
||||
isNormalUser = true;
|
||||
};
|
||||
groups.worblehat = { };
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
services.worblehat.settings = lib.pipe ../config-template.toml [
|
||||
builtins.readFile
|
||||
builtins.fromTOML
|
||||
@@ -102,147 +69,123 @@ in {
|
||||
})
|
||||
(lib.mapAttrsRecursive (_: lib.mkDefault))
|
||||
];
|
||||
})
|
||||
}
|
||||
{
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
|
||||
(lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
environment.etc."worblehat/config.toml".source = format.generate "worblehat-config.toml" cfg.settings;
|
||||
|
||||
services.worblehat.settings.database.type = "postgresql";
|
||||
services.worblehat.settings.database.postgresql = {
|
||||
host = "/run/postgresql";
|
||||
};
|
||||
|
||||
services.postgresql = lib.mkIf cfg.createLocalDatabase {
|
||||
ensureDatabases = [ "worblehat" ];
|
||||
ensureUsers = [{
|
||||
name = "worblehat";
|
||||
ensureDBOwnership = true;
|
||||
ensureClauses.login = true;
|
||||
}];
|
||||
};
|
||||
|
||||
systemd.services.worblehat-setup-database = lib.mkIf cfg.createLocalDatabase {
|
||||
description = "Dibbler database setup";
|
||||
wantedBy = [ "default.target" ];
|
||||
after = [ "postgresql.service" ];
|
||||
unitConfig = {
|
||||
ConditionPathExists = "!/var/lib/worblehat/.db-setup-done";
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${lib.getExe cfg.package} --config /etc/worblehat/config.toml create-db";
|
||||
ExecStartPost = "${lib.getExe' pkgs.coreutils "touch"} /var/lib/worblehat/.db-setup-done";
|
||||
StateDirectory = "worblehat";
|
||||
|
||||
User = "worblehat";
|
||||
Group = "worblehat";
|
||||
};
|
||||
};
|
||||
}
|
||||
(lib.mkIf cfg.kioskMode {
|
||||
boot.kernelParams = [
|
||||
"console=tty1"
|
||||
];
|
||||
|
||||
users.users.worblehat = {
|
||||
extraGroups = [ "lp" ];
|
||||
shell = (pkgs.writeShellScriptBin "login-shell" "${lib.getExe' cfg.screenPackage "screen"} -x worblehat") // {
|
||||
shellPath = "/bin/login-shell";
|
||||
};
|
||||
};
|
||||
|
||||
services.worblehat.settings.general = {
|
||||
quit_allowed = false;
|
||||
stop_allowed = false;
|
||||
};
|
||||
|
||||
systemd.services.worblehat-screen-session = {
|
||||
description = "Worblehat Screen Session";
|
||||
wantedBy = [
|
||||
"default.target"
|
||||
];
|
||||
after = if cfg.createLocalDatabase then [
|
||||
"postgresql.service"
|
||||
"worblehat-setup-database.service"
|
||||
] else [
|
||||
"network.target"
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "forking";
|
||||
RemainAfterExit = false;
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
SuccessExitStatus = 1;
|
||||
|
||||
User = "worblehat";
|
||||
Group = "worblehat";
|
||||
|
||||
ExecStartPre = "-${lib.getExe' cfg.screenPackage "screen"} -X -S worblehat kill";
|
||||
ExecStart = let
|
||||
screenArgs = lib.escapeShellArgs [
|
||||
# -dm creates the screen in detached mode without accessing it
|
||||
"-dm"
|
||||
|
||||
# Session name
|
||||
"-S"
|
||||
"worblehat"
|
||||
|
||||
# Set optimal output mode instead of VT100 emulation
|
||||
"-O"
|
||||
|
||||
# Enable login mode, updates utmp entries
|
||||
"-l"
|
||||
];
|
||||
|
||||
worblehatArgs = lib.cli.toCommandLineShellGNU { } {
|
||||
config = "/etc/worblehat/config.toml";
|
||||
};
|
||||
|
||||
in "${lib.getExe' cfg.screenPackage "screen"} ${screenArgs} ${lib.getExe cfg.package} ${worblehatArgs} cli";
|
||||
ExecStartPost =
|
||||
lib.optionals (cfg.limitScreenWidth != null) [
|
||||
"${lib.getExe' cfg.screenPackage "screen"} -X -S worblehat width ${toString cfg.limitScreenWidth}"
|
||||
]
|
||||
++ lib.optionals (cfg.limitScreenHeight != null) [
|
||||
"${lib.getExe' cfg.screenPackage "screen"} -X -S worblehat height ${toString cfg.limitScreenHeight}"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
services.getty.autologinUser = "worblehat";
|
||||
})
|
||||
]))
|
||||
|
||||
(lib.mkIf cfg.deadline-daemon.enable {
|
||||
systemd.timers.worblehat-deadline-daemon = {
|
||||
description = "Worblehat Deadline Daemon";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.deadline-daemon.onCalendar;
|
||||
Persistent = true;
|
||||
users = {
|
||||
users.worblehat = {
|
||||
group = "worblehat";
|
||||
isNormalUser = true;
|
||||
};
|
||||
groups.worblehat = { };
|
||||
};
|
||||
|
||||
systemd.services.worblehat-deadline-daemon = {
|
||||
description = "Worblehat Deadline Daemon";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
services.worblehat.settings.database.type = "postgresql";
|
||||
services.worblehat.settings.database.postgresql = {
|
||||
host = "/run/postgresql";
|
||||
};
|
||||
|
||||
services.postgresql = lib.mkIf cfg.createLocalDatabase {
|
||||
ensureDatabases = [ "worblehat" ];
|
||||
ensureUsers = [{
|
||||
name = "worblehat";
|
||||
ensureDBOwnership = true;
|
||||
ensureClauses.login = true;
|
||||
}];
|
||||
};
|
||||
|
||||
systemd.services.worblehat-setup-database = lib.mkIf cfg.createLocalDatabase {
|
||||
description = "Dibbler database setup";
|
||||
wantedBy = [ "default.target" ];
|
||||
after = [ "postgresql.service" ];
|
||||
unitConfig = {
|
||||
ConditionPathExists = "!/var/lib/worblehat/.db-setup-done";
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
CPUSchedulingPolicy = "idle";
|
||||
IOSchedulingClass = "idle";
|
||||
|
||||
ExecStart = let
|
||||
worblehatArgs = lib.cli.toCommandLineShellGNU { } {
|
||||
config = "/etc/worblehat/config.toml";
|
||||
};
|
||||
in "${lib.getExe cfg.package} ${worblehatArgs} deadline-daemon";
|
||||
ExecStart = "${lib.getExe cfg.package} --config /etc/worblehat/config.toml create-db";
|
||||
ExecStartPost = "${lib.getExe' pkgs.coreutils "touch"} /var/lib/worblehat/.db-setup-done";
|
||||
StateDirectory = "worblehat";
|
||||
|
||||
User = "worblehat";
|
||||
Group = "worblehat";
|
||||
};
|
||||
};
|
||||
}
|
||||
(lib.mkIf cfg.kioskMode {
|
||||
boot.kernelParams = [
|
||||
"console=tty1"
|
||||
];
|
||||
|
||||
users.users.worblehat = {
|
||||
extraGroups = [ "lp" ];
|
||||
shell = (pkgs.writeShellScriptBin "login-shell" "${lib.getExe cfg.screenPackage} -x worblehat") // {
|
||||
shellPath = "/bin/login-shell";
|
||||
};
|
||||
};
|
||||
|
||||
services.worblehat.settings.general = {
|
||||
quit_allowed = false;
|
||||
stop_allowed = false;
|
||||
};
|
||||
|
||||
systemd.services.worblehat-screen-session = {
|
||||
description = "Worblehat Screen Session";
|
||||
wantedBy = [
|
||||
"default.target"
|
||||
];
|
||||
after = if cfg.createLocalDatabase then [
|
||||
"postgresql.service"
|
||||
"worblehat-setup-database.service"
|
||||
] else [
|
||||
"network.target"
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "forking";
|
||||
RemainAfterExit = false;
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
SuccessExitStatus = 1;
|
||||
|
||||
User = "worblehat";
|
||||
Group = "worblehat";
|
||||
|
||||
ExecStartPre = "-${lib.getExe cfg.screenPackage} -X -S worblehat kill";
|
||||
ExecStart = let
|
||||
screenArgs = lib.escapeShellArgs [
|
||||
# -dm creates the screen in detached mode without accessing it
|
||||
"-dm"
|
||||
|
||||
# Session name
|
||||
"-S"
|
||||
"worblehat"
|
||||
|
||||
# Set optimal output mode instead of VT100 emulation
|
||||
"-O"
|
||||
|
||||
# Enable login mode, updates utmp entries
|
||||
"-l"
|
||||
];
|
||||
|
||||
worblehatArgs = lib.cli.toCommandLineShellGNU { } {
|
||||
config = "/etc/worblehat/config.toml";
|
||||
};
|
||||
|
||||
in "${lib.getExe cfg.screenPackage} ${screenArgs} ${lib.getExe cfg.package} ${worblehatArgs} cli";
|
||||
ExecStartPost =
|
||||
lib.optionals (cfg.limitScreenWidth != null) [
|
||||
"${lib.getExe cfg.screenPackage} -X -S worblehat width ${toString cfg.limitScreenWidth}"
|
||||
]
|
||||
++ lib.optionals (cfg.limitScreenHeight != null) [
|
||||
"${lib.getExe cfg.screenPackage} -X -S worblehat height ${toString cfg.limitScreenHeight}"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
services.getty.autologinUser = "worblehat";
|
||||
})
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ nixpkgs.lib.nixosSystem {
|
||||
services.worblehat = {
|
||||
enable = true;
|
||||
createLocalDatabase = true;
|
||||
deadline-daemon.enable = true;
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
@@ -29,7 +29,6 @@ nixpkgs.lib.nixosSystem {
|
||||
DEBUG = true;
|
||||
};
|
||||
};
|
||||
deadline-daemon.enable = true;
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
@@ -7,10 +7,10 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"alembic>=1.16",
|
||||
"alembic>=1.17",
|
||||
"beautifulsoup4>=4.14",
|
||||
"click>=8.2",
|
||||
"flask-admin>=1.6",
|
||||
"click>=8.3",
|
||||
"flask-admin>=2.0",
|
||||
"flask-sqlalchemy>=3.1",
|
||||
"flask>=3.0",
|
||||
"isbnlib>=3.10",
|
||||
|
||||
170
src/worblehat/devscripts/batch_scanner.py
Normal file
170
src/worblehat/devscripts/batch_scanner.py
Normal file
@@ -0,0 +1,170 @@
|
||||
from worblehat.cli.subclis import select_bookcase_shelf
|
||||
from sqlalchemy import select, event
|
||||
from sqlalchemy.orm.session import Session
|
||||
from worblehat.models import Bookcase, BookcaseShelf
|
||||
from worblehat.services.metadata_fetchers import fetch_metadata_from_multiple_sources
|
||||
from worblehat.services import is_valid_isbn
|
||||
from pprint import pprint
|
||||
from libdib.repl import (
|
||||
NumberedCmd,
|
||||
prompt_yes_no,
|
||||
)
|
||||
|
||||
class BatchScanner(NumberedCmd):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__()
|
||||
self.sql_session = sql_session
|
||||
self.sql_session_dirty = False
|
||||
self.bookcase_shelf = None
|
||||
self.prompt_header = "[SHELF| NONE]"
|
||||
|
||||
@event.listens_for(self.sql_session, "after_flush")
|
||||
def mark_session_as_dirty(*_):
|
||||
self.sql_session_dirty = True
|
||||
self.prompt_header = "(unsaved changes)"
|
||||
|
||||
@event.listens_for(self.sql_session, "after_commit")
|
||||
@event.listens_for(self.sql_session, "after_rollback")
|
||||
def mark_session_as_clean(*_):
|
||||
self.sql_session_dirty = False
|
||||
self.prompt_header = None
|
||||
|
||||
|
||||
def _set_bookcase_shelf(self, bookcase_shelf: BookcaseShelf):
|
||||
self.bookcase_shelf = bookcase_shelf
|
||||
self.prompt_header = f"[SHELF| {bookcase_shelf.bookcase.uid} {bookcase_shelf.bookcase.description} / {bookcase_shelf.column}-{bookcase_shelf.row}: {bookcase_shelf.description}]"
|
||||
|
||||
|
||||
def default(self, isbn: str):
|
||||
isbn = isbn.strip()
|
||||
if not is_valid_isbn(isbn):
|
||||
super()._default(isbn)
|
||||
return
|
||||
|
||||
if self.bookcase_shelf is None:
|
||||
print("Please set the bookcase shelf first.")
|
||||
return
|
||||
|
||||
print(f"Scanned ISBN: {isbn}")
|
||||
|
||||
data = fetch_metadata_from_multiple_sources(isbn)
|
||||
|
||||
pprint(data)
|
||||
|
||||
|
||||
def do_set_bookcase_shelf(self, _: str):
|
||||
bookcase = self._choose_bookcase(self.sql_session)
|
||||
|
||||
print()
|
||||
|
||||
self._print_bookcase_shelves(
|
||||
sql_session=self.sql_session,
|
||||
bookcase=bookcase,
|
||||
)
|
||||
|
||||
print()
|
||||
|
||||
bookcase_shelf = select_bookcase_shelf(
|
||||
sql_session=self.sql_session,
|
||||
bookcase=bookcase,
|
||||
)
|
||||
|
||||
self._set_bookcase_shelf(bookcase_shelf)
|
||||
|
||||
print(f"Bookcase shelf set to {bookcase_shelf}")
|
||||
|
||||
def _choose_bookcase(
|
||||
self,
|
||||
sql_session: Session,
|
||||
) -> Bookcase:
|
||||
bookcases = sql_session.scalars(
|
||||
select(Bookcase)
|
||||
).all()
|
||||
|
||||
while True:
|
||||
print("Available bookcases:")
|
||||
for bookcase in bookcases:
|
||||
print(f" {bookcase.name} - {bookcase.description}")
|
||||
|
||||
bookcase_name = input("Choose a bookcase> ").strip()
|
||||
|
||||
bookcase = sql_session.scalars(
|
||||
select(Bookcase).where(Bookcase.name == bookcase_name)
|
||||
).one_or_none()
|
||||
|
||||
if not bookcase:
|
||||
print(f"Bookcase {bookcase_name} not found")
|
||||
continue
|
||||
|
||||
return bookcase
|
||||
|
||||
def _print_bookcase_shelves(
|
||||
self,
|
||||
sql_session: Session,
|
||||
bookcase: Bookcase,
|
||||
) -> None:
|
||||
shelves = sql_session.scalars(
|
||||
select(BookcaseShelf).where(BookcaseShelf.bookcase == bookcase)
|
||||
).all()
|
||||
min_col = min([shelf.column for shelf in shelves])
|
||||
max_col = max([shelf.column for shelf in shelves])
|
||||
min_row = min([shelf.row for shelf in shelves])
|
||||
max_row = max([shelf.row for shelf in shelves])
|
||||
|
||||
print('Available shelves:')
|
||||
for col in range(min_col, max_col + 1):
|
||||
for row in range(min_row, max_row + 1):
|
||||
shelf = sql_session.scalars(
|
||||
select(BookcaseShelf).where(
|
||||
BookcaseShelf.bookcase == bookcase,
|
||||
BookcaseShelf.column == col,
|
||||
BookcaseShelf.row == row,
|
||||
)
|
||||
).one_or_none()
|
||||
if shelf:
|
||||
print(f"{col}-{row}: {shelf.description}")
|
||||
else:
|
||||
print(f"{col}-{row}: None")
|
||||
|
||||
def do_save(self, _: str):
|
||||
if not self.sql_session_dirty:
|
||||
print("No changes to save.")
|
||||
return
|
||||
self.sql_session.commit()
|
||||
|
||||
def do_abort(self, _: str):
|
||||
if not self.sql_session_dirty:
|
||||
print("No changes to abort.")
|
||||
return
|
||||
self.sql_session.rollback()
|
||||
|
||||
def do_exit(self, _: str):
|
||||
if self.sql_session_dirty:
|
||||
if prompt_yes_no("Would you like to save your changes?"):
|
||||
self.sql_session.commit()
|
||||
else:
|
||||
self.sql_session.rollback()
|
||||
exit(0)
|
||||
|
||||
funcs = {
|
||||
0: {
|
||||
"f": default,
|
||||
"doc": "Add item with its ISBN",
|
||||
},
|
||||
1: {
|
||||
'f' : do_set_bookcase_shelf,
|
||||
'doc' : 'Select shelf',
|
||||
},
|
||||
7: {
|
||||
"f": do_save,
|
||||
"doc": "Save changes",
|
||||
},
|
||||
8: {
|
||||
"f": do_abort,
|
||||
"doc": "Abort changes",
|
||||
},
|
||||
9: {
|
||||
"f": do_exit,
|
||||
"doc": "Exit",
|
||||
},
|
||||
}
|
||||
@@ -85,6 +85,12 @@ def main() -> None:
|
||||
from .devscripts.seed_test_data import main
|
||||
|
||||
main(sql_session)
|
||||
elif args.script == "batch-scanner":
|
||||
from .devscripts.batch_scanner import BatchScanner
|
||||
|
||||
result = BatchScanner(sql_session).cmdloop()
|
||||
|
||||
print(result)
|
||||
else:
|
||||
print(devscripts_arg_parser.format_help())
|
||||
exit(1)
|
||||
|
||||
@@ -52,6 +52,11 @@ devscripts_subparsers.add_parser(
|
||||
help="Seed data tailorded for testing the deadline daemon, into the database",
|
||||
)
|
||||
|
||||
devscripts_subparsers.add_parser(
|
||||
"batch-scanner",
|
||||
help="REPL optimized for scanning in tons of books",
|
||||
)
|
||||
|
||||
arg_parser.add_argument(
|
||||
"-V",
|
||||
"--version",
|
||||
|
||||
@@ -2,7 +2,7 @@ import tomllib
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Any
|
||||
import os
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
@@ -38,14 +38,10 @@ class Config:
|
||||
|
||||
@staticmethod
|
||||
def read_password(password_field: str) -> str:
|
||||
file: Path = Path(password_field)
|
||||
if 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]):
|
||||
if Path(password_field).is_file():
|
||||
with Path(password_field).open() as f:
|
||||
return f.read().strip()
|
||||
return f.read()
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Testing, should only use file. {password_field}",
|
||||
)
|
||||
return password_field
|
||||
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user