Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ebe4154ac | ||
|
|
9033dfc7dc | ||
|
|
0ce79cda52 | ||
|
|
7b5446a824 |
51
README.md
51
README.md
@@ -24,13 +24,12 @@ Worblehatt har vært påbegynnt flere ganger opp gjennom historien uten å komme
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
This project uses `uv` as its buildtool as of February 2025.
|
This project uses [poetry][poetry] as its buildtool as of May 2023.
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ uv run alembic -x config=./config-template.toml upgrade head
|
$ poetry install
|
||||||
$ uv run worblehat -c config-template.toml devscripts seed-test-data
|
$ poetry run alembic migrate
|
||||||
$ uv run worblehat --help
|
$ poetry run worblehat --help
|
||||||
$ uv run worblehat -c config-template.toml cli
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to configure
|
## How to configure
|
||||||
@@ -43,4 +42,44 @@ Unless provided through the `--config` flag, program will automatically look for
|
|||||||
- `~/.config/worblehat/config.toml`
|
- `~/.config/worblehat/config.toml`
|
||||||
- `/var/lib/worblehat/config.toml`
|
- `/var/lib/worblehat/config.toml`
|
||||||
|
|
||||||
Run `uv run worblehat --help` for more info
|
Run `poetry run worblehat --help` for more info
|
||||||
|
|
||||||
|
## TODO List
|
||||||
|
|
||||||
|
### Setting up a database with all of PVVs books
|
||||||
|
|
||||||
|
- [ ] Create postgres database
|
||||||
|
- [ ] Model all bookshelfs
|
||||||
|
- [ ] Scan in all books
|
||||||
|
|
||||||
|
### Cli version of the program (this is currently being worked on)
|
||||||
|
|
||||||
|
- [X] Ability to pull data from online sources with ISBN
|
||||||
|
- [X] Ability to create and update bookcases
|
||||||
|
- [X] Ability to create and update bookcase shelfs
|
||||||
|
- [X] Ability to create and update bookcase items
|
||||||
|
- [X] Ability to borrow and deliver items
|
||||||
|
- [ ] Ability to borrow and deliver multiple items at a time
|
||||||
|
- [X] Ability to enter the queue for borrowing an item
|
||||||
|
- [ ] Ability to extend a borrowing, only if no one is behind you in the queue
|
||||||
|
- [ ] Ability to list borrowed items which are overdue
|
||||||
|
- [~] Ability to search for items
|
||||||
|
- [ ] Ability to print PVV-specific labels for items missing a label, or which for any other reason needs a custom one
|
||||||
|
- [X] Ascii art of monkey with fingers in eyes
|
||||||
|
|
||||||
|
### Deadline daemon
|
||||||
|
|
||||||
|
- [X] Ability to be notified when deadlines are due
|
||||||
|
- [ ] Ability to be notified when books are available
|
||||||
|
- [ ] Ability to have expiring queue positions automatically expire
|
||||||
|
|
||||||
|
### Web version of the program
|
||||||
|
|
||||||
|
- [ ] Ability for PVV members to search for books through the PVV website
|
||||||
|
|
||||||
|
## Points of discussion
|
||||||
|
|
||||||
|
- Should this project run in a separate tty-instance on Dibblers interface, or should they share the tty with some kind of switching ability?
|
||||||
|
After some discussion with other PVV members, we came up with an idea where we run the programs in separate ttys, and use a set of large mechanical switches connected to a QMK-flashed microcontroller to switch between them.
|
||||||
|
|
||||||
|
- Workaround for not being able to represent items with same ISBN and different owner: if you are absolutely adamant about placing your item at PVV while still owning it, even though PVV already owns a copy of this item, please print out a new label with a "PVV-ISBN" for it
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[alembic]
|
[alembic]
|
||||||
# path to migration scripts
|
# path to migration scripts
|
||||||
script_location = src/worblehat/models/migrations
|
script_location = worblehat/models/migrations
|
||||||
|
|
||||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||||
# Uncomment the line below if you want the files to be prepended with date and time
|
# Uncomment the line below if you want the files to be prepended with date and time
|
||||||
|
|||||||
@@ -33,8 +33,5 @@ from = 'worblehat@pvv.ntnu.no'
|
|||||||
subject_prefix = '[Worblehat]'
|
subject_prefix = '[Worblehat]'
|
||||||
|
|
||||||
[deadline_daemon]
|
[deadline_daemon]
|
||||||
enabled = true
|
warn_days_before_borrow_deadline = [ "5", "1" ]
|
||||||
dryrun = false
|
warn_days_before_expiring_queue_position_deadline = [ "3", "1" ]
|
||||||
warn_days_before_borrowing_deadline = [ 5, 1 ]
|
|
||||||
days_before_queue_position_expires = 14
|
|
||||||
warn_days_before_expiring_queue_position_deadline = [ 3, 1 ]
|
|
||||||
@@ -1,29 +1,33 @@
|
|||||||
isbn,note,bookcase,shelf
|
isbn, note, bookcase, shelf
|
||||||
9780486809038,emily riehl,arbeidsrom_smal,5
|
9780486809038, emily riehl, arbeidsrom_smal, 5
|
||||||
9781568811307,winning ways,arbeidsrom_smal,5
|
9781568811307, winning ways, arbeidsrom_smal, 5
|
||||||
9780486458731,cardano,arbeidsrom_smal,5
|
9780486458731, cardano, arbeidsrom_smal, 5
|
||||||
9780486462394,alg topo,arbeidsrom_smal,5
|
9780486462394, alg topo, arbeidsrom_smal, 5
|
||||||
9780582447585,formulae,arbeidsrom_smal,5
|
9780582447585, formulae, arbeidsrom_smal, 5
|
||||||
9780486466668,theory of numbers,arbeidsrom_smal,5
|
9780486466668, theory of numbers, arbeidsrom_smal, 5
|
||||||
9780486462431,conv surf,arbeidsrom_smal,5
|
9780486462431, conv surf, arbeidsrom_smal, 5
|
||||||
9780486449685,math fun and earnest,arbeidsrom_smal,5
|
9780486449685, math fun and earnest, arbeidsrom_smal, 5
|
||||||
9780486417103,lin prog,arbeidsrom_smal,5
|
9780486417103, lin prog, arbeidsrom_smal, 5
|
||||||
9780130892393,complex analysis,arbeidsrom_smal,5
|
9780130892393, complex analysis, arbeidsrom_smal, 5
|
||||||
9781292024967,abstract alg,arbeidsrom_smal,5
|
9781292024967, abstract alg, arbeidsrom_smal, 5
|
||||||
9780471728979,kreyzig,arbeidsrom_smal,5
|
9780471728979, kreyzig, arbeidsrom_smal, 5
|
||||||
9781847762399,calc 1,arbeidsrom_smal,5
|
9781847762399, calc 1, arbeidsrom_smal, 5
|
||||||
9781787267763,calc 1 nome,arbeidsrom_smal,5
|
9781847762399, calc 1 again, arbeidsrom_smal, 5
|
||||||
9781787267770,calc 2 nome,arbeidsrom_smal,5
|
9781787267763, calc 1 nome, arbeidsrom_smal, 5
|
||||||
9780199208258,non lin ode,arbeidsrom_smal,5
|
9781787267770, calc 2 nome, arbeidsrom_smal, 5
|
||||||
9788251915953,tabeller,arbeidsrom_smal,5
|
9780199208258, non lin ode, arbeidsrom_smal, 5
|
||||||
9780750304009,fractals and chaos,arbeidsrom_smal,5
|
9788251915953, tabeller, arbeidsrom_smal, 5
|
||||||
9788241902116,geometri,arbeidsrom_smal,5
|
9788251915953, taeller 2, arbeidsrom_smal, 5
|
||||||
9781620402788,simpsons,arbeidsrom_smal,5
|
9788251915953, tabeller 3, arbeidsrom_smal, 5
|
||||||
9781846683459,math curiosities,arbeidsrom_smal,5
|
9788251915953, tabeller 4, arbeidsrom_smal, 5
|
||||||
9789810245344,fuzzy logic,arbeidsrom_smal,5
|
9780750304009, fractals and chaos, arbeidsrom_smal, 5
|
||||||
9781429224048,vect calc,arbeidsrom_smal,5
|
9788241902116, geometri, arbeidsrom_smal, 5
|
||||||
9780122407611,gambling,arbeidsrom_smal,5
|
9781620402788, simpsons, arbeidsrom_smal, 5
|
||||||
9788278220054,rottman slitt,arbeidsrom_smal,5
|
9781846683459, math curiosities, arbeidsrom_smal, 5
|
||||||
9780321748232,prob and stat,arbeidsrom_smal,5
|
9789810245344, fuzzy logic, arbeidsrom_smal, 5
|
||||||
9780387954752,stats with r,arbeidsrom_smal,5
|
9781429224048, vect calc, arbeidsrom_smal, 5
|
||||||
9781568814421,maths by exp,arbeidsrom_smal,5
|
9780122407611, gambling, arbeidsrom_smal, 5
|
||||||
|
9788278220054, rottman slitt, arbeidsrom_smal, 5
|
||||||
|
9780321748232, prob and stat, arbeidsrom_smal, 5
|
||||||
|
9780387954752, stats with r, arbeidsrom_smal, 5
|
||||||
|
9781568814421, maths by exp, arbeidsrom_smal, 5
|
||||||
|
|||||||
|
67
flake.lock
generated
67
flake.lock
generated
@@ -1,79 +1,24 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "flake-utils",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"libdib": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1765185678,
|
|
||||||
"narHash": "sha256-WOZFtsWO/oDwCEprWTpvwwehzPEvyvTgeEXobAA3bwI=",
|
|
||||||
"ref": "refs/heads/main",
|
|
||||||
"rev": "5fa4863057de272afa1b79379e7ba105c19fdb4b",
|
|
||||||
"revCount": 10,
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://git.pvv.ntnu.no/Projects/libdib.git"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://git.pvv.ntnu.no/Projects/libdib.git"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1764947035,
|
"lastModified": 1683014792,
|
||||||
"narHash": "sha256-EYHSjVM4Ox4lvCXUMiKKs2vETUSL5mx+J2FfutM7T9w=",
|
"narHash": "sha256-6Va9iVtmmsw4raBc3QKvQT2KT/NGRWlvUlJj46zN8B8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a672be65651c80d3f592a89b3945466584a22069",
|
"rev": "1a411f23ba299db155a5b45d5e145b85a7aafc42",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"id": "nixpkgs",
|
||||||
"ref": "nixpkgs-unstable",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"type": "indirect"
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"libdib": "libdib",
|
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
106
flake.nix
106
flake.nix
@@ -1,93 +1,43 @@
|
|||||||
{
|
{
|
||||||
inputs = {
|
# inputs.nixpkgs.url = "nixpkgs/nixos-22.11";
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
libdib.url = "git+https://git.pvv.ntnu.no/Projects/libdib.git";
|
|
||||||
libdib.inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = inputs@{ self, nixpkgs, ... }: let
|
|
||||||
systems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
|
|
||||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: let
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
overlays = [
|
|
||||||
inputs.libdib.overlays.default
|
|
||||||
];
|
|
||||||
};
|
|
||||||
in f system pkgs);
|
|
||||||
|
|
||||||
inherit (nixpkgs) lib;
|
|
||||||
|
|
||||||
deps = ppkgs: with ppkgs; [
|
|
||||||
alembic
|
|
||||||
beautifulsoup4
|
|
||||||
click
|
|
||||||
flask
|
|
||||||
(flask-admin.override {
|
|
||||||
wtf-peewee = wtf-peewee.overrideAttrs (_: {
|
|
||||||
doCheck = false;
|
|
||||||
doInstallCheck = false;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
flask-sqlalchemy
|
|
||||||
isbnlib
|
|
||||||
libdib
|
|
||||||
psycopg2-binary
|
|
||||||
python-dotenv
|
|
||||||
requests
|
|
||||||
sqlalchemy
|
|
||||||
];
|
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }: let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
inherit (pkgs) lib;
|
||||||
in {
|
in {
|
||||||
apps = forAllSystems (system: pkgs: let
|
apps.${system} = let
|
||||||
mkApp = package: {
|
app = program: {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = lib.getExe package;
|
inherit program;
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
default = mkApp self.packages.${system}.default;
|
default = self.apps.${system}.worblehat;
|
||||||
});
|
worblehat = app "${self.packages.${system}.worblehat}/bin/worblehat";
|
||||||
|
|
||||||
devShells = forAllSystems (_: pkgs: {
|
|
||||||
default = pkgs.mkShell {
|
|
||||||
packages = with pkgs; [
|
|
||||||
uv
|
|
||||||
ruff
|
|
||||||
sqlite-interactive
|
|
||||||
(python3.withPackages deps)
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
overlays.default = final: prev: self.packages.${final.system};
|
packages.${system} = {
|
||||||
|
|
||||||
packages = forAllSystems (system: pkgs: {
|
|
||||||
default = self.packages.${system}.worblehat;
|
default = self.packages.${system}.worblehat;
|
||||||
worblehat = let
|
worblehat = with pkgs.python3Packages; buildPythonPackage {
|
||||||
inherit (pkgs) python3Packages;
|
pname = "worblehat";
|
||||||
pyproject = lib.pipe ./pyproject.toml [
|
version = "0.1.0";
|
||||||
builtins.readFile
|
src = ./.;
|
||||||
builtins.fromTOML
|
|
||||||
];
|
|
||||||
in python3Packages.buildPythonApplication {
|
|
||||||
pname = pyproject.project.name;
|
|
||||||
version = pyproject.project.version;
|
|
||||||
src = lib.cleanSource ./.;
|
|
||||||
|
|
||||||
format = "pyproject";
|
format = "pyproject";
|
||||||
|
|
||||||
build-system = with python3Packages; [ hatchling ];
|
buildInputs = [ poetry-core ];
|
||||||
dependencies = deps pkgs.python3Packages;
|
propagatedBuildInputs = [
|
||||||
|
alembic
|
||||||
meta.mainProgram = "worblehat";
|
click
|
||||||
|
flask
|
||||||
|
flask-admin
|
||||||
|
flask-sqlalchemy
|
||||||
|
isbnlib
|
||||||
|
python-dotenv
|
||||||
|
sqlalchemy
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
1139
poetry.lock
generated
Normal file
1139
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,31 @@
|
|||||||
[project]
|
[tool.poetry]
|
||||||
name = "worblehat"
|
name = "worblehat"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Worblehat is a simple library management system written specifically for Programvareverkstedet"
|
description = "Worblehat is a simple library management system written specifically for Programvareverkstedet"
|
||||||
authors = []
|
authors = []
|
||||||
license = { file = "LICENSE" }
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
|
||||||
dependencies = [
|
|
||||||
"alembic>=1.17",
|
|
||||||
"beautifulsoup4>=4.14",
|
|
||||||
"click>=8.3",
|
|
||||||
"flask-admin>=2.0",
|
|
||||||
"flask-sqlalchemy>=3.1",
|
|
||||||
"flask>=3.0",
|
|
||||||
"isbnlib>=3.10",
|
|
||||||
"libdib",
|
|
||||||
"psycopg2-binary>=2.9",
|
|
||||||
"requests>=2.32",
|
|
||||||
"sqlalchemy>=2.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependency-groups]
|
[tool.poetry.dependencies]
|
||||||
dev = [
|
alembic = "^1.9.4"
|
||||||
"werkzeug",
|
click = "^8.1.3"
|
||||||
"poethepoet",
|
flask = "^2.2.2"
|
||||||
]
|
flask-admin = "^1.6.1"
|
||||||
|
flask-sqlalchemy = "^3.0.3"
|
||||||
|
isbnlib = "^3.10.14"
|
||||||
|
python = "^3.11"
|
||||||
|
sqlalchemy = "^2.0.8"
|
||||||
|
psycopg2-binary = "^2.9.6"
|
||||||
|
requests = "^2.31.0"
|
||||||
|
bs4 = "^0.0.1"
|
||||||
|
thestorygraph-client = "^0.1.6"
|
||||||
|
|
||||||
[project.scripts]
|
[tool.poetry.group.dev.dependencies]
|
||||||
worblehat = "worblehat:main"
|
werkzeug = "^2.3.3"
|
||||||
|
poethepoet = "^0.20.0"
|
||||||
|
|
||||||
[build-system]
|
[tool.poetry.scripts]
|
||||||
requires = ["hatchling"]
|
worblehat = "worblehat.main:main"
|
||||||
build-backend = "hatchling.build"
|
|
||||||
|
|
||||||
[tool.poe.tasks]
|
[tool.poe.tasks]
|
||||||
clean = """
|
clean = """
|
||||||
@@ -48,5 +42,7 @@ downmigrate = "alembic downgrade -1"
|
|||||||
# delete the migration file with this, there will be no easy way of downgrading
|
# delete the migration file with this, there will be no easy way of downgrading
|
||||||
cleanmigrations = "git clean -f worblehat/models/migrations/versions"
|
cleanmigrations = "git clean -f worblehat/models/migrations/versions"
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
libdib = { git = "https://git.pvv.ntnu.no/Projects/libdib.git" }
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
from .main import main
|
|
||||||
|
|
||||||
__all__ = ["main"]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .main import WorblehatCli
|
|
||||||
|
|
||||||
__all__ = ["WorblehatCli"]
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
from textwrap import dedent
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import (
|
|
||||||
event,
|
|
||||||
select,
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from libdib.repl import (
|
|
||||||
NumberedCmd,
|
|
||||||
InteractiveItemSelector,
|
|
||||||
prompt_yes_no,
|
|
||||||
)
|
|
||||||
|
|
||||||
from worblehat.services import (
|
|
||||||
create_bookcase_item_from_isbn,
|
|
||||||
is_valid_isbn,
|
|
||||||
)
|
|
||||||
|
|
||||||
from worblehat.models import *
|
|
||||||
|
|
||||||
from .subclis import (
|
|
||||||
AdvancedOptionsCli,
|
|
||||||
BookcaseItemCli,
|
|
||||||
select_bookcase_shelf,
|
|
||||||
SearchCli,
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: Category seems to have been forgotten. Maybe relevant interactivity should be added?
|
|
||||||
# However, is there anyone who are going to search by category rather than just look in
|
|
||||||
# the shelves?
|
|
||||||
|
|
||||||
|
|
||||||
class WorblehatCli(NumberedCmd):
|
|
||||||
def __init__(self, sql_session: Session):
|
|
||||||
super().__init__()
|
|
||||||
self.sql_session = sql_session
|
|
||||||
self.sql_session_dirty = False
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def run_with_safe_exit_wrapper(cls, sql_session: Session):
|
|
||||||
tool = cls(sql_session)
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
tool.cmdloop()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
if not tool.sql_session_dirty:
|
|
||||||
exit(0)
|
|
||||||
try:
|
|
||||||
print()
|
|
||||||
if prompt_yes_no(
|
|
||||||
"Are you sure you want to exit without saving?", default=False
|
|
||||||
):
|
|
||||||
raise KeyboardInterrupt
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
if tool.sql_session is not None:
|
|
||||||
tool.sql_session.rollback()
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
def do_show_bookcase(self, arg: str):
|
|
||||||
bookcase_selector = InteractiveItemSelector(
|
|
||||||
cls=Bookcase,
|
|
||||||
sql_session=self.sql_session,
|
|
||||||
)
|
|
||||||
bookcase_selector.cmdloop()
|
|
||||||
bookcase = bookcase_selector.result
|
|
||||||
|
|
||||||
for shelf in bookcase.shelfs:
|
|
||||||
print(shelf.short_str())
|
|
||||||
for item in shelf.items:
|
|
||||||
print(f" {item.name} - {item.amount} copies")
|
|
||||||
|
|
||||||
def do_show_borrowed_queued(self, _: str):
|
|
||||||
borrowed_items = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowing)
|
|
||||||
.where(BookcaseItemBorrowing.delivered.is_(None))
|
|
||||||
.order_by(BookcaseItemBorrowing.end_time),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(borrowed_items) == 0:
|
|
||||||
print("No borrowed items found.")
|
|
||||||
else:
|
|
||||||
print("Borrowed items:")
|
|
||||||
for item in borrowed_items:
|
|
||||||
print(
|
|
||||||
f"- {item.username} - {item.item.name} - to be delivered by {item.end_time.strftime('%Y-%m-%d')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
queued_items = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowingQueue).order_by(
|
|
||||||
BookcaseItemBorrowingQueue.entered_queue_time
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(queued_items) == 0:
|
|
||||||
print("No queued items found.")
|
|
||||||
else:
|
|
||||||
print("Users in queue:")
|
|
||||||
for item in queued_items:
|
|
||||||
print(
|
|
||||||
f"- {item.username} - {item.item.name} - entered queue at {item.entered_queue_time.strftime('%Y-%m-%d')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_bookcase_item(self, isbn: str):
|
|
||||||
bookcase_item = create_bookcase_item_from_isbn(isbn, self.sql_session)
|
|
||||||
if bookcase_item is None:
|
|
||||||
print(f"Could not find data about item with ISBN {isbn} online.")
|
|
||||||
print(
|
|
||||||
"If you think this is not due to a bug, please add the book to openlibrary.org before continuing."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
dedent(f"""
|
|
||||||
Found item:
|
|
||||||
title: {bookcase_item.name}
|
|
||||||
authors: {", ".join(a.name for a in bookcase_item.authors)}
|
|
||||||
language: {bookcase_item.language}
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
|
|
||||||
print("Please select the bookcase where the item is placed:")
|
|
||||||
bookcase_selector = InteractiveItemSelector(
|
|
||||||
cls=Bookcase,
|
|
||||||
sql_session=self.sql_session,
|
|
||||||
)
|
|
||||||
bookcase_selector.cmdloop()
|
|
||||||
bookcase = bookcase_selector.result
|
|
||||||
|
|
||||||
bookcase_item.shelf = select_bookcase_shelf(bookcase, self.sql_session)
|
|
||||||
|
|
||||||
print("Please select the items media type:")
|
|
||||||
media_type_selector = InteractiveItemSelector(
|
|
||||||
cls=MediaType,
|
|
||||||
sql_session=self.sql_session,
|
|
||||||
default=self.sql_session.scalars(
|
|
||||||
select(MediaType).where(MediaType.name.ilike("book")),
|
|
||||||
).one(),
|
|
||||||
)
|
|
||||||
|
|
||||||
media_type_selector.cmdloop()
|
|
||||||
bookcase_item.media_type = media_type_selector.result
|
|
||||||
|
|
||||||
username = input("Who owns this book? [PVV]> ")
|
|
||||||
if username != "":
|
|
||||||
bookcase_item.owner = username
|
|
||||||
|
|
||||||
self.sql_session.add(bookcase_item)
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def default(self, isbn: str):
|
|
||||||
isbn = isbn.strip()
|
|
||||||
if not is_valid_isbn(isbn):
|
|
||||||
super()._default(isbn)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (
|
|
||||||
existing_item := self.sql_session.scalars(
|
|
||||||
select(BookcaseItem)
|
|
||||||
.where(BookcaseItem.isbn == isbn)
|
|
||||||
.join(BookcaseItemBorrowing)
|
|
||||||
.join(BookcaseItemBorrowingQueue)
|
|
||||||
).one_or_none()
|
|
||||||
) is not None:
|
|
||||||
print(f'\nFound existing item for isbn "{isbn}"')
|
|
||||||
BookcaseItemCli(
|
|
||||||
sql_session=self.sql_session,
|
|
||||||
bookcase_item=existing_item,
|
|
||||||
).cmdloop()
|
|
||||||
return
|
|
||||||
|
|
||||||
if prompt_yes_no(
|
|
||||||
f"Could not find item with ISBN '{isbn}'.\nWould you like to create it?",
|
|
||||||
default=True,
|
|
||||||
):
|
|
||||||
self._create_bookcase_item(isbn)
|
|
||||||
|
|
||||||
def do_search(self, _: str):
|
|
||||||
search_cli = SearchCli(self.sql_session)
|
|
||||||
search_cli.cmdloop()
|
|
||||||
if search_cli.result is not None:
|
|
||||||
BookcaseItemCli(
|
|
||||||
sql_session=self.sql_session,
|
|
||||||
bookcase_item=search_cli.result,
|
|
||||||
).cmdloop()
|
|
||||||
|
|
||||||
def do_show_slabbedasker(self, _: str):
|
|
||||||
slubberter = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowing)
|
|
||||||
.join(BookcaseItem)
|
|
||||||
.where(
|
|
||||||
BookcaseItemBorrowing.end_time < datetime.now(),
|
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
|
||||||
)
|
|
||||||
.order_by(
|
|
||||||
BookcaseItemBorrowing.end_time,
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(slubberter) == 0:
|
|
||||||
print("No slubberts found. Life is good.")
|
|
||||||
return
|
|
||||||
|
|
||||||
for slubbert in slubberter:
|
|
||||||
print("Slubberter:")
|
|
||||||
print(
|
|
||||||
f"- {slubbert.username} - {slubbert.item.name} - {slubbert.end_time.strftime('%Y-%m-%d')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_advanced(self, _: str):
|
|
||||||
AdvancedOptionsCli(self.sql_session).cmdloop()
|
|
||||||
|
|
||||||
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": "Choose / Add item with its ISBN",
|
|
||||||
},
|
|
||||||
1: {
|
|
||||||
"f": do_search,
|
|
||||||
"doc": "Search",
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
"f": do_show_bookcase,
|
|
||||||
"doc": "Show a bookcase, and its items",
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
"f": do_show_borrowed_queued,
|
|
||||||
"doc": "Show borrowed/queued items",
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
"f": do_show_slabbedasker,
|
|
||||||
"doc": "Show slabbedasker",
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
"f": do_save,
|
|
||||||
"doc": "Save changes",
|
|
||||||
},
|
|
||||||
6: {
|
|
||||||
"f": do_abort,
|
|
||||||
"doc": "Abort changes",
|
|
||||||
},
|
|
||||||
7: {
|
|
||||||
"f": do_advanced,
|
|
||||||
"doc": "Advanced options",
|
|
||||||
},
|
|
||||||
9: {
|
|
||||||
"f": do_exit,
|
|
||||||
"doc": "Exit",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from libdib.repl import (
|
|
||||||
InteractiveItemSelector,
|
|
||||||
NumberedCmd,
|
|
||||||
)
|
|
||||||
from worblehat.models import Bookcase, BookcaseShelf
|
|
||||||
|
|
||||||
|
|
||||||
class AdvancedOptionsCli(NumberedCmd):
|
|
||||||
def __init__(self, sql_session: Session):
|
|
||||||
super().__init__()
|
|
||||||
self.sql_session = sql_session
|
|
||||||
|
|
||||||
def do_add_bookcase(self, _: str):
|
|
||||||
while True:
|
|
||||||
name = input("Name of bookcase> ")
|
|
||||||
if name == "":
|
|
||||||
print("Error: name cannot be empty")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (
|
|
||||||
self.sql_session.scalars(
|
|
||||||
select(Bookcase).where(Bookcase.name == name)
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
print(f"Error: a bookcase with name {name} already exists")
|
|
||||||
continue
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
description = input("Description of bookcase> ")
|
|
||||||
if description == "":
|
|
||||||
description = None
|
|
||||||
|
|
||||||
bookcase = Bookcase(name, description)
|
|
||||||
self.sql_session.add(bookcase)
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def do_add_bookcase_shelf(self, arg: str):
|
|
||||||
bookcase_selector = InteractiveItemSelector(
|
|
||||||
cls=Bookcase,
|
|
||||||
sql_session=self.sql_session,
|
|
||||||
)
|
|
||||||
bookcase_selector.cmdloop()
|
|
||||||
bookcase = bookcase_selector.result
|
|
||||||
|
|
||||||
while True:
|
|
||||||
column = input("Column> ")
|
|
||||||
try:
|
|
||||||
column = int(column)
|
|
||||||
except ValueError:
|
|
||||||
print("Error: column must be a number")
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
|
|
||||||
while True:
|
|
||||||
row = input("Row> ")
|
|
||||||
try:
|
|
||||||
row = int(row)
|
|
||||||
except ValueError:
|
|
||||||
print("Error: row must be a number")
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
|
|
||||||
if (
|
|
||||||
self.sql_session.scalars(
|
|
||||||
select(BookcaseShelf).where(
|
|
||||||
BookcaseShelf.bookcase == bookcase,
|
|
||||||
BookcaseShelf.column == column,
|
|
||||||
BookcaseShelf.row == row,
|
|
||||||
)
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
print(
|
|
||||||
f"Error: a bookshelf in bookcase {bookcase.name} with position c{column}-r{row} already exists"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
description = input("Description> ")
|
|
||||||
if description == "":
|
|
||||||
description = None
|
|
||||||
|
|
||||||
shelf = BookcaseShelf(
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
bookcase,
|
|
||||||
description,
|
|
||||||
)
|
|
||||||
self.sql_session.add(shelf)
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def do_list_bookcases(self, _: str):
|
|
||||||
bookcase_shelfs = self.sql_session.scalars(
|
|
||||||
select(BookcaseShelf)
|
|
||||||
.join(Bookcase)
|
|
||||||
.order_by(
|
|
||||||
Bookcase.name,
|
|
||||||
BookcaseShelf.column,
|
|
||||||
BookcaseShelf.row,
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
bookcase_uid = None
|
|
||||||
for shelf in bookcase_shelfs:
|
|
||||||
if shelf.bookcase.uid != bookcase_uid:
|
|
||||||
print(shelf.bookcase.short_str())
|
|
||||||
bookcase_uid = shelf.bookcase.uid
|
|
||||||
|
|
||||||
print(f" {shelf.short_str()} - {sum(i.amount for i in shelf.items)} items")
|
|
||||||
|
|
||||||
def do_done(self, _: str):
|
|
||||||
return True
|
|
||||||
|
|
||||||
funcs = {
|
|
||||||
1: {
|
|
||||||
"f": do_add_bookcase,
|
|
||||||
"doc": "Add bookcase",
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
"f": do_add_bookcase_shelf,
|
|
||||||
"doc": "Add bookcase shelf",
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
"f": do_list_bookcases,
|
|
||||||
"doc": "List all bookcases",
|
|
||||||
},
|
|
||||||
9: {
|
|
||||||
"f": do_done,
|
|
||||||
"doc": "Done",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,416 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
from textwrap import dedent
|
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from libdib.repl import (
|
|
||||||
InteractiveItemSelector,
|
|
||||||
NumberedCmd,
|
|
||||||
NumberedItemSelector,
|
|
||||||
format_date,
|
|
||||||
prompt_yes_no,
|
|
||||||
)
|
|
||||||
from worblehat.models import (
|
|
||||||
Bookcase,
|
|
||||||
BookcaseItem,
|
|
||||||
BookcaseItemBorrowing,
|
|
||||||
BookcaseItemBorrowingQueue,
|
|
||||||
Language,
|
|
||||||
MediaType,
|
|
||||||
)
|
|
||||||
from worblehat.services.bookcase_item import (
|
|
||||||
create_bookcase_item_from_isbn,
|
|
||||||
is_valid_isbn,
|
|
||||||
)
|
|
||||||
from worblehat.services.config import Config
|
|
||||||
|
|
||||||
from .bookcase_shelf_selector import select_bookcase_shelf
|
|
||||||
|
|
||||||
|
|
||||||
def _selected_bookcase_item_prompt(bookcase_item: BookcaseItem) -> str:
|
|
||||||
amount_borrowed = len(bookcase_item.borrowings)
|
|
||||||
return dedent(f"""
|
|
||||||
Item: {bookcase_item.name}
|
|
||||||
ISBN: {bookcase_item.isbn}
|
|
||||||
Authors: {", ".join(a.name for a in bookcase_item.authors)}
|
|
||||||
Bookcase: {bookcase_item.shelf.bookcase.short_str()}
|
|
||||||
Shelf: {bookcase_item.shelf.short_str()}
|
|
||||||
Amount: {bookcase_item.amount - amount_borrowed}/{bookcase_item.amount}
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
class BookcaseItemCli(NumberedCmd):
|
|
||||||
def __init__(self, sql_session: Session, bookcase_item: BookcaseItem):
|
|
||||||
super().__init__()
|
|
||||||
self.sql_session = sql_session
|
|
||||||
self.bookcase_item = bookcase_item
|
|
||||||
|
|
||||||
@property
|
|
||||||
def prompt_header(self) -> str:
|
|
||||||
return _selected_bookcase_item_prompt(self.bookcase_item)
|
|
||||||
|
|
||||||
def do_update_data(self, _: str):
|
|
||||||
item = create_bookcase_item_from_isbn(str(self.bookcase_item.isbn), self.sql_session)
|
|
||||||
|
|
||||||
if item is None:
|
|
||||||
print("Error: could not fetch metadata for this item")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.bookcase_item.name = item.name
|
|
||||||
# TODO: Remove any old authors
|
|
||||||
self.bookcase_item.authors = item.authors
|
|
||||||
self.bookcase_item.language = item.language
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def do_edit(self, arg: str):
|
|
||||||
EditBookcaseCli(self.sql_session, self.bookcase_item, self).cmdloop()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _prompt_username() -> str:
|
|
||||||
while True:
|
|
||||||
username = input("Username: ")
|
|
||||||
if prompt_yes_no(f"Is {username} correct?", default=True):
|
|
||||||
return username
|
|
||||||
|
|
||||||
def _has_active_borrowing(self, username: str) -> bool:
|
|
||||||
return (
|
|
||||||
self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowing).where(
|
|
||||||
BookcaseItemBorrowing.username == username,
|
|
||||||
BookcaseItemBorrowing.item == self.bookcase_item,
|
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
|
||||||
)
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
def _has_borrowing_queue_item(self, username: str) -> bool:
|
|
||||||
return (
|
|
||||||
self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowingQueue).where(
|
|
||||||
BookcaseItemBorrowingQueue.username == username,
|
|
||||||
BookcaseItemBorrowingQueue.item == self.bookcase_item,
|
|
||||||
)
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_borrow(self, _: str):
|
|
||||||
active_borrowings = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowing)
|
|
||||||
.where(
|
|
||||||
BookcaseItemBorrowing.item == self.bookcase_item,
|
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
|
||||||
)
|
|
||||||
.order_by(BookcaseItemBorrowing.end_time)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(active_borrowings) >= self.bookcase_item.amount:
|
|
||||||
print("This item is currently not available")
|
|
||||||
print()
|
|
||||||
print("Active borrowings:")
|
|
||||||
|
|
||||||
for b in active_borrowings:
|
|
||||||
print(f" {b.username} - Until {format_date(b.end_time)}")
|
|
||||||
|
|
||||||
if len(self.bookcase_item.borrowing_queue) > 0:
|
|
||||||
print("Borrowing queue:")
|
|
||||||
for i, b in enumerate(self.bookcase_item.borrowing_queue):
|
|
||||||
print(f" {i + 1} - {b.username}")
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
if not prompt_yes_no(
|
|
||||||
"Would you like to enter the borrowing queue?", default=True
|
|
||||||
):
|
|
||||||
return
|
|
||||||
username = self._prompt_username()
|
|
||||||
|
|
||||||
if self._has_active_borrowing(username):
|
|
||||||
print("You already have an active borrowing")
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._has_borrowing_queue_item(username):
|
|
||||||
print("You are already in the borrowing queue")
|
|
||||||
return
|
|
||||||
|
|
||||||
borrowing_queue_item = BookcaseItemBorrowingQueue(
|
|
||||||
username, self.bookcase_item
|
|
||||||
)
|
|
||||||
self.sql_session.add(borrowing_queue_item)
|
|
||||||
print(f"{username} entered the queue!")
|
|
||||||
return
|
|
||||||
|
|
||||||
username = self._prompt_username()
|
|
||||||
|
|
||||||
borrowing_item = BookcaseItemBorrowing(username, self.bookcase_item)
|
|
||||||
self.sql_session.add(borrowing_item)
|
|
||||||
self.sql_session.flush()
|
|
||||||
print(
|
|
||||||
f"Successfully borrowed the item. Please deliver it back by {format_date(borrowing_item.end_time)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_deliver(self, _: str):
|
|
||||||
borrowings = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowing)
|
|
||||||
.join(
|
|
||||||
BookcaseItem,
|
|
||||||
BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid,
|
|
||||||
)
|
|
||||||
.where(BookcaseItem.isbn == self.bookcase_item.isbn)
|
|
||||||
.order_by(BookcaseItemBorrowing.username)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(borrowings) == 0:
|
|
||||||
print("No one seems to have borrowed this item")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Borrowers:")
|
|
||||||
for i, b in enumerate(borrowings):
|
|
||||||
print(f" {i + 1}) {b.username}")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
selection = int(input("> "))
|
|
||||||
except ValueError:
|
|
||||||
print("Error: selection must be an integer")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if selection < 1 or selection > len(borrowings):
|
|
||||||
print("Error: selection out of range")
|
|
||||||
continue
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
borrowing = borrowings[selection - 1]
|
|
||||||
borrowing.delivered = datetime.now()
|
|
||||||
self.sql_session.flush()
|
|
||||||
print(f"Successfully delivered the item for {borrowing.username}")
|
|
||||||
|
|
||||||
def do_extend_borrowing(self, _: str):
|
|
||||||
borrowings = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowing)
|
|
||||||
.join(
|
|
||||||
BookcaseItem,
|
|
||||||
BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid,
|
|
||||||
)
|
|
||||||
.where(BookcaseItem.isbn == self.bookcase_item.isbn)
|
|
||||||
.order_by(BookcaseItemBorrowing.username)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(borrowings) == 0:
|
|
||||||
print("No one seems to have borrowed this item")
|
|
||||||
return
|
|
||||||
|
|
||||||
borrowing_queue = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowingQueue)
|
|
||||||
.where(
|
|
||||||
BookcaseItemBorrowingQueue.item == self.bookcase_item,
|
|
||||||
BookcaseItemBorrowingQueue.item_became_available_time == None,
|
|
||||||
)
|
|
||||||
.order_by(BookcaseItemBorrowingQueue.entered_queue_time)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(borrowing_queue) != 0:
|
|
||||||
print(
|
|
||||||
"Sorry, you cannot extend the borrowing because there are people waiting in the queue"
|
|
||||||
)
|
|
||||||
print("Borrowing queue:")
|
|
||||||
for i, b in enumerate(borrowing_queue):
|
|
||||||
print(f" {i + 1}) {b.username}")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Who are you?")
|
|
||||||
selector = NumberedItemSelector(
|
|
||||||
items=list(borrowings),
|
|
||||||
stringify=lambda b: f"{b.username} - Until {format_date(b.end_time)}",
|
|
||||||
)
|
|
||||||
selector.cmdloop()
|
|
||||||
if selector.result is None:
|
|
||||||
return
|
|
||||||
borrowing = selector.result
|
|
||||||
|
|
||||||
borrowing.end_time = datetime.now() + timedelta(
|
|
||||||
days=int(Config["deadline_daemon.days_before_queue_position_expires"])
|
|
||||||
)
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Successfully extended the borrowing for {borrowing.username} until {format_date(borrowing.end_time)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_done(self, _: str):
|
|
||||||
return True
|
|
||||||
|
|
||||||
funcs = {
|
|
||||||
1: {
|
|
||||||
"f": do_borrow,
|
|
||||||
"doc": "Borrow",
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
"f": do_deliver,
|
|
||||||
"doc": "Deliver",
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
"f": do_extend_borrowing,
|
|
||||||
"doc": "Extend borrowing",
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
"f": do_edit,
|
|
||||||
"doc": "Edit",
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
"f": do_update_data,
|
|
||||||
"doc": "Pull updated data from online databases",
|
|
||||||
},
|
|
||||||
9: {
|
|
||||||
"f": do_done,
|
|
||||||
"doc": "Done",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class EditBookcaseCli(NumberedCmd):
|
|
||||||
def __init__(
|
|
||||||
self, sql_session: Session, bookcase_item: BookcaseItem, parent: BookcaseItemCli
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
self.sql_session = sql_session
|
|
||||||
self.bookcase_item = bookcase_item
|
|
||||||
self.parent = parent
|
|
||||||
|
|
||||||
@property
|
|
||||||
def prompt_header(self) -> str:
|
|
||||||
return _selected_bookcase_item_prompt(self.bookcase_item)
|
|
||||||
|
|
||||||
def do_name(self, _: str):
|
|
||||||
while True:
|
|
||||||
name = input("New name> ")
|
|
||||||
if name == "":
|
|
||||||
print("Error: name cannot be empty")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (
|
|
||||||
self.sql_session.scalars(
|
|
||||||
select(BookcaseItem).where(BookcaseItem.name == name)
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
print(f"Error: an item with name {name} already exists")
|
|
||||||
continue
|
|
||||||
|
|
||||||
break
|
|
||||||
self.bookcase_item.name = name
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def do_isbn(self, _: str):
|
|
||||||
while True:
|
|
||||||
isbn = input("New ISBN> ")
|
|
||||||
if isbn == "":
|
|
||||||
print("Error: ISBN cannot be empty")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not is_valid_isbn(isbn):
|
|
||||||
print("Error: ISBN is not valid")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if (
|
|
||||||
self.sql_session.scalars(
|
|
||||||
select(BookcaseItem).where(BookcaseItem.isbn == isbn)
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
print(f"Error: an item with ISBN {isbn} already exists")
|
|
||||||
continue
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
self.bookcase_item.isbn = isbn
|
|
||||||
|
|
||||||
if prompt_yes_no("Update data from online databases?"):
|
|
||||||
self.parent.do_update_data("")
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def do_language(self, _: str):
|
|
||||||
language_selector = InteractiveItemSelector(
|
|
||||||
Language,
|
|
||||||
self.sql_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.bookcase_item.language = language_selector.result
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def do_media_type(self, _: str):
|
|
||||||
media_type_selector = InteractiveItemSelector(
|
|
||||||
MediaType,
|
|
||||||
self.sql_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.bookcase_item.media_type = media_type_selector.result
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def do_amount(self, _: str):
|
|
||||||
while (
|
|
||||||
new_amount := input(f"New amount [{self.bookcase_item.amount}]> ")
|
|
||||||
) != "":
|
|
||||||
try:
|
|
||||||
new_amount = int(new_amount)
|
|
||||||
except ValueError:
|
|
||||||
print("Error: amount must be an integer")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if new_amount < 1:
|
|
||||||
print("Error: amount must be greater than 0")
|
|
||||||
continue
|
|
||||||
|
|
||||||
break
|
|
||||||
self.bookcase_item.amount = new_amount
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def do_shelf(self, _: str):
|
|
||||||
bookcase_selector = InteractiveItemSelector(
|
|
||||||
Bookcase,
|
|
||||||
self.sql_session,
|
|
||||||
)
|
|
||||||
bookcase_selector.cmdloop()
|
|
||||||
bookcase = bookcase_selector.result
|
|
||||||
|
|
||||||
shelf = select_bookcase_shelf(bookcase, self.sql_session)
|
|
||||||
|
|
||||||
self.bookcase_item.shelf = shelf
|
|
||||||
self.sql_session.flush()
|
|
||||||
|
|
||||||
def do_done(self, _: str):
|
|
||||||
return True
|
|
||||||
|
|
||||||
funcs = {
|
|
||||||
1: {
|
|
||||||
"f": do_name,
|
|
||||||
"doc": "Change name",
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
"f": do_isbn,
|
|
||||||
"doc": "Change ISBN",
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
"f": do_language,
|
|
||||||
"doc": "Change language",
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
"f": do_media_type,
|
|
||||||
"doc": "Change media type",
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
"f": do_amount,
|
|
||||||
"doc": "Change amount",
|
|
||||||
},
|
|
||||||
6: {
|
|
||||||
"f": do_shelf,
|
|
||||||
"doc": "Change shelf",
|
|
||||||
},
|
|
||||||
9: {
|
|
||||||
"f": do_done,
|
|
||||||
"doc": "Done",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .main import DeadlineDaemon
|
|
||||||
|
|
||||||
__all__ = ["DeadlineDaemon"]
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from textwrap import dedent
|
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from worblehat.services.config import Config
|
|
||||||
from worblehat.models import (
|
|
||||||
BookcaseItemBorrowing,
|
|
||||||
DeadlineDaemonLastRunDatetime,
|
|
||||||
BookcaseItemBorrowingQueue,
|
|
||||||
)
|
|
||||||
|
|
||||||
from worblehat.services.email import send_email
|
|
||||||
|
|
||||||
|
|
||||||
class DeadlineDaemon:
|
|
||||||
def __init__(self, sql_session: Session):
|
|
||||||
if not Config["deadline_daemon.enabled"]:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.sql_session = sql_session
|
|
||||||
|
|
||||||
self.last_run = self.sql_session.scalars(
|
|
||||||
select(DeadlineDaemonLastRunDatetime),
|
|
||||||
).one_or_none()
|
|
||||||
|
|
||||||
if self.last_run is None:
|
|
||||||
logging.info("No previous run found, assuming this is the first run")
|
|
||||||
self.last_run = DeadlineDaemonLastRunDatetime(time=datetime.now())
|
|
||||||
self.sql_session.add(self.last_run)
|
|
||||||
self.sql_session.commit()
|
|
||||||
|
|
||||||
self.last_run_datetime = self.last_run.time
|
|
||||||
self.current_run_datetime = datetime.now()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
logging.info("Deadline daemon started")
|
|
||||||
if not Config["deadline_daemon.enabled"]:
|
|
||||||
logging.warn("Deadline daemon disabled, exiting")
|
|
||||||
return
|
|
||||||
|
|
||||||
if Config["deadline_daemon.dryrun"]:
|
|
||||||
logging.warn("Running in dryrun mode")
|
|
||||||
|
|
||||||
self.send_close_deadline_reminder_mails()
|
|
||||||
self.send_overdue_mails()
|
|
||||||
self.send_newly_available_mails()
|
|
||||||
self.send_expiring_queue_position_mails()
|
|
||||||
self.auto_expire_queue_positions()
|
|
||||||
|
|
||||||
self.last_run.time = self.current_run_datetime
|
|
||||||
self.sql_session.commit()
|
|
||||||
|
|
||||||
###################
|
|
||||||
# EMAIL TEMPLATES #
|
|
||||||
###################
|
|
||||||
|
|
||||||
def _send_close_deadline_mail(self, borrowing: BookcaseItemBorrowing):
|
|
||||||
logging.info(
|
|
||||||
f"Sending close deadline mail to {borrowing.username}@pvv.ntnu.no."
|
|
||||||
)
|
|
||||||
send_email(
|
|
||||||
f"{borrowing.username}@pvv.ntnu.no",
|
|
||||||
"Reminder - Your borrowing deadline is approaching",
|
|
||||||
dedent(
|
|
||||||
f"""
|
|
||||||
Your borrowing deadline for the following item is approaching:
|
|
||||||
|
|
||||||
{borrowing.item.name}
|
|
||||||
|
|
||||||
Please return the item by {borrowing.end_time.strftime("%a %b %d, %Y")}
|
|
||||||
""",
|
|
||||||
).strip(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _send_overdue_mail(self, borrowing: BookcaseItemBorrowing):
|
|
||||||
logging.info(
|
|
||||||
f"Sending overdue mail to {borrowing.username}@pvv.ntnu.no for {borrowing.item.isbn} - {borrowing.end_time.strftime('%a %b %d, %Y')}"
|
|
||||||
)
|
|
||||||
send_email(
|
|
||||||
f"{borrowing.username}@pvv.ntnu.no",
|
|
||||||
"Your deadline has passed",
|
|
||||||
dedent(
|
|
||||||
f"""
|
|
||||||
Your delivery deadline for the following item has passed:
|
|
||||||
|
|
||||||
{borrowing.item.name}
|
|
||||||
|
|
||||||
Please return the item as soon as possible.
|
|
||||||
""",
|
|
||||||
).strip(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _send_newly_available_mail(self, queue_item: BookcaseItemBorrowingQueue):
|
|
||||||
logging.info(f"Sending newly available mail to {queue_item.username}")
|
|
||||||
|
|
||||||
days_before_queue_expires = Config[
|
|
||||||
"deadline_daemon.days_before_queue_position_expires"
|
|
||||||
]
|
|
||||||
|
|
||||||
# TODO: calculate and format the date of when the queue position expires in the mail.
|
|
||||||
send_email(
|
|
||||||
f"{queue_item.username}@pvv.ntnu.no",
|
|
||||||
"An item you have queued for is now available",
|
|
||||||
dedent(
|
|
||||||
f"""
|
|
||||||
The following item is now available for you to borrow:
|
|
||||||
|
|
||||||
{queue_item.item.name}
|
|
||||||
|
|
||||||
Please pick up the item within {days_before_queue_expires} days.
|
|
||||||
""",
|
|
||||||
).strip(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _send_expiring_queue_position_mail(
|
|
||||||
self, queue_position: BookcaseItemBorrowingQueue, day: int
|
|
||||||
):
|
|
||||||
logging.info(
|
|
||||||
f"Sending queue position expiry reminder to {queue_position.username}@pvv.ntnu.no."
|
|
||||||
)
|
|
||||||
send_email(
|
|
||||||
f"{queue_position.username}@pvv.ntnu.no",
|
|
||||||
"Reminder - Your queue position expiry deadline is approaching",
|
|
||||||
dedent(
|
|
||||||
f"""
|
|
||||||
Your queue position expiry deadline for the following item is approaching:
|
|
||||||
|
|
||||||
{queue_position.item.name}
|
|
||||||
|
|
||||||
Please borrow the item by {(queue_position.item_became_available_time + timedelta(days=day)).strftime("%a %b %d, %Y")}
|
|
||||||
""",
|
|
||||||
).strip(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _send_queue_position_expired_mail(
|
|
||||||
self, queue_position: BookcaseItemBorrowingQueue
|
|
||||||
):
|
|
||||||
send_email(
|
|
||||||
f"{queue_position.username}@pvv.ntnu.no",
|
|
||||||
"Your queue position has expired",
|
|
||||||
dedent(
|
|
||||||
f"""
|
|
||||||
Your queue position for the following item has expired:
|
|
||||||
|
|
||||||
{queue_position.item.name}
|
|
||||||
|
|
||||||
You can queue for the item again at any time, but you will be placed at the back of the queue.
|
|
||||||
|
|
||||||
There are currently {len(queue_position.item.borrowing_queue)} users in the queue.
|
|
||||||
""",
|
|
||||||
).strip(),
|
|
||||||
)
|
|
||||||
|
|
||||||
##################
|
|
||||||
# EMAIL ROUTINES #
|
|
||||||
##################
|
|
||||||
|
|
||||||
def _sql_subtract_date(self, x: datetime, y: timedelta):
|
|
||||||
if self.sql_session.bind.dialect.name == "sqlite":
|
|
||||||
# SQLite does not support timedelta in queries
|
|
||||||
return func.datetime(x, f"-{y.days} days")
|
|
||||||
elif self.sql_session.bind.dialect.name == "postgresql":
|
|
||||||
return x - y
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
f"Unsupported dialect: {self.sql_session.bind.dialect.name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_close_deadline_reminder_mails(self):
|
|
||||||
logging.info("Sending mails for items with a closing deadline")
|
|
||||||
|
|
||||||
# TODO: This should be int-parsed and validated before the daemon started
|
|
||||||
days = [
|
|
||||||
int(d)
|
|
||||||
for d in Config["deadline_daemon.warn_days_before_borrowing_deadline"]
|
|
||||||
]
|
|
||||||
|
|
||||||
for day in days:
|
|
||||||
borrowings_to_remind = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowing).where(
|
|
||||||
self._sql_subtract_date(
|
|
||||||
BookcaseItemBorrowing.end_time,
|
|
||||||
timedelta(days=day),
|
|
||||||
).between(
|
|
||||||
self.last_run_datetime,
|
|
||||||
self.current_run_datetime,
|
|
||||||
),
|
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
for borrowing in borrowings_to_remind:
|
|
||||||
self._send_close_deadline_mail(borrowing)
|
|
||||||
|
|
||||||
def send_overdue_mails(self):
|
|
||||||
logging.info("Sending mails for overdue items")
|
|
||||||
|
|
||||||
to_remind = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowing).where(
|
|
||||||
BookcaseItemBorrowing.end_time < self.current_run_datetime,
|
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for borrowing in to_remind:
|
|
||||||
self._send_overdue_mail(borrowing)
|
|
||||||
|
|
||||||
def send_newly_available_mails(self):
|
|
||||||
logging.info("Sending mails about newly available items")
|
|
||||||
|
|
||||||
newly_available = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowingQueue)
|
|
||||||
.join(
|
|
||||||
BookcaseItemBorrowing,
|
|
||||||
BookcaseItemBorrowing.fk_bookcase_item_uid
|
|
||||||
== BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
BookcaseItemBorrowingQueue.expired.is_(False),
|
|
||||||
BookcaseItemBorrowing.delivered.is_not(None),
|
|
||||||
BookcaseItemBorrowing.delivered.between(
|
|
||||||
self.last_run_datetime,
|
|
||||||
self.current_run_datetime,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.order_by(BookcaseItemBorrowingQueue.entered_queue_time)
|
|
||||||
.group_by(BookcaseItemBorrowingQueue.fk_bookcase_item_uid)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for queue_item in newly_available:
|
|
||||||
logging.info(
|
|
||||||
f"Adding user {queue_item.username} to queue for {queue_item.item.name}"
|
|
||||||
)
|
|
||||||
queue_item.item_became_available_time = self.current_run_datetime
|
|
||||||
self.sql_session.commit()
|
|
||||||
|
|
||||||
self._send_newly_available_mail(queue_item)
|
|
||||||
|
|
||||||
def send_expiring_queue_position_mails(self):
|
|
||||||
logging.info("Sending mails about queue positions which are expiring soon")
|
|
||||||
logging.warning("Not implemented")
|
|
||||||
|
|
||||||
days = [
|
|
||||||
int(d)
|
|
||||||
for d in Config[
|
|
||||||
"deadline_daemon.warn_days_before_expiring_queue_position_deadline"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
for day in days:
|
|
||||||
queue_positions_to_remind = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowingQueue)
|
|
||||||
.join(
|
|
||||||
BookcaseItemBorrowing,
|
|
||||||
BookcaseItemBorrowing.fk_bookcase_item_uid
|
|
||||||
== BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
self._sql_subtract_date(
|
|
||||||
BookcaseItemBorrowingQueue.item_became_available_time
|
|
||||||
+ timedelta(days=day),
|
|
||||||
timedelta(days=day),
|
|
||||||
).between(
|
|
||||||
self.last_run_datetime,
|
|
||||||
self.current_run_datetime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for queue_position in queue_positions_to_remind:
|
|
||||||
self._send_expiring_queue_position_mail(queue_position, day)
|
|
||||||
|
|
||||||
def auto_expire_queue_positions(self):
|
|
||||||
logging.info("Expiring queue positions which are too old")
|
|
||||||
|
|
||||||
queue_position_expiry_days = int(
|
|
||||||
Config["deadline_daemon.days_before_queue_position_expires"]
|
|
||||||
)
|
|
||||||
|
|
||||||
overdue_queue_positions = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowingQueue).where(
|
|
||||||
BookcaseItemBorrowingQueue.item_became_available_time
|
|
||||||
+ timedelta(days=queue_position_expiry_days)
|
|
||||||
< self.current_run_datetime,
|
|
||||||
BookcaseItemBorrowingQueue.expired.is_(False),
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for queue_position in overdue_queue_positions:
|
|
||||||
logging.info(
|
|
||||||
f"Expiring queue position for {queue_position.username} for item {queue_position.item.name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
queue_position.expired = True
|
|
||||||
|
|
||||||
next_queue_position = self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowingQueue)
|
|
||||||
.where(
|
|
||||||
BookcaseItemBorrowingQueue.fk_bookcase_item_uid
|
|
||||||
== queue_position.fk_bookcase_item_uid,
|
|
||||||
BookcaseItemBorrowingQueue.item_became_available_time.is_(None),
|
|
||||||
)
|
|
||||||
.order_by(BookcaseItemBorrowingQueue.entered_queue_time)
|
|
||||||
.limit(1),
|
|
||||||
).one_or_none()
|
|
||||||
|
|
||||||
self._send_queue_position_expired_mail(queue_position)
|
|
||||||
|
|
||||||
if next_queue_position is not None:
|
|
||||||
next_queue_position.item_became_available_time = (
|
|
||||||
self.current_run_datetime
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.info(
|
|
||||||
f"Next user in queue for item {next_queue_position.item.name} is {next_queue_position.username}"
|
|
||||||
)
|
|
||||||
self._send_newly_available_mail(next_queue_position)
|
|
||||||
|
|
||||||
self.sql_session.commit()
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from worblehat.models import (
|
|
||||||
BookcaseItem,
|
|
||||||
BookcaseItemBorrowing,
|
|
||||||
BookcaseItemBorrowingQueue,
|
|
||||||
DeadlineDaemonLastRunDatetime,
|
|
||||||
)
|
|
||||||
|
|
||||||
from worblehat.services.config import Config
|
|
||||||
|
|
||||||
from .seed_test_data import main as seed_test_data_main
|
|
||||||
|
|
||||||
|
|
||||||
def clear_db(sql_session):
|
|
||||||
sql_session.query(BookcaseItemBorrowingQueue).delete()
|
|
||||||
sql_session.query(BookcaseItemBorrowing).delete()
|
|
||||||
sql_session.query(DeadlineDaemonLastRunDatetime).delete()
|
|
||||||
sql_session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE: feel free to change this function to suit your needs
|
|
||||||
# it's just a quick and dirty way to get some data into the database
|
|
||||||
# for testing the deadline daemon - oysteikt 2024
|
|
||||||
def main(sql_session):
|
|
||||||
borrow_warning_days = [
|
|
||||||
timedelta(days=int(d))
|
|
||||||
for d in Config["deadline_daemon.warn_days_before_borrowing_deadline"]
|
|
||||||
]
|
|
||||||
queue_warning_days = [
|
|
||||||
timedelta(days=int(d))
|
|
||||||
for d in Config[
|
|
||||||
"deadline_daemon.warn_days_before_expiring_queue_position_deadline"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
queue_expire_days = int(
|
|
||||||
Config["deadline_daemon.days_before_queue_position_expires"]
|
|
||||||
)
|
|
||||||
|
|
||||||
clear_db(sql_session)
|
|
||||||
seed_test_data_main(sql_session)
|
|
||||||
|
|
||||||
books = sql_session.query(BookcaseItem).all()
|
|
||||||
|
|
||||||
last_run_datetime = datetime.now() - timedelta(days=16)
|
|
||||||
last_run = DeadlineDaemonLastRunDatetime(last_run_datetime)
|
|
||||||
sql_session.add(last_run)
|
|
||||||
|
|
||||||
# Create at least one item that is borrowed and not supposed to be returned yet
|
|
||||||
borrowing = BookcaseItemBorrowing(
|
|
||||||
item=books[0],
|
|
||||||
username="test_borrower_still_borrowing",
|
|
||||||
)
|
|
||||||
borrowing.start_time = last_run_datetime - timedelta(days=1)
|
|
||||||
borrowing.end_time = datetime.now() - timedelta(days=6)
|
|
||||||
sql_session.add(borrowing)
|
|
||||||
|
|
||||||
# Create at least one item that is borrowed and is supposed to be returned soon
|
|
||||||
borrowing = BookcaseItemBorrowing(
|
|
||||||
item=books[1],
|
|
||||||
username="test_borrower_return_soon",
|
|
||||||
)
|
|
||||||
borrowing.start_time = last_run_datetime - timedelta(days=1)
|
|
||||||
borrowing.end_time = datetime.now() - timedelta(days=2)
|
|
||||||
sql_session.add(borrowing)
|
|
||||||
|
|
||||||
# Create at least one item that is borrowed and is overdue
|
|
||||||
borrowing = BookcaseItemBorrowing(
|
|
||||||
item=books[2],
|
|
||||||
username="test_borrower_overdue",
|
|
||||||
)
|
|
||||||
borrowing.start_time = datetime.now() - timedelta(days=1)
|
|
||||||
borrowing.end_time = datetime.now() + timedelta(days=1)
|
|
||||||
sql_session.add(borrowing)
|
|
||||||
|
|
||||||
# Create at least one item that is in the queue and is not supposed to be borrowed yet
|
|
||||||
queue_item = BookcaseItemBorrowingQueue(
|
|
||||||
item=books[3],
|
|
||||||
username="test_queue_user_still_waiting",
|
|
||||||
)
|
|
||||||
queue_item.entered_queue_time = last_run_datetime - timedelta(days=1)
|
|
||||||
borrowing = BookcaseItemBorrowing(
|
|
||||||
item=books[3],
|
|
||||||
username="test_borrower_return_soon",
|
|
||||||
)
|
|
||||||
borrowing.start_time = last_run_datetime - timedelta(days=1)
|
|
||||||
borrowing.end_time = datetime.now() - timedelta(days=2)
|
|
||||||
sql_session.add(queue_item)
|
|
||||||
sql_session.add(borrowing)
|
|
||||||
|
|
||||||
# Create at least three items that is in the queue and two items were just returned
|
|
||||||
for i in range(3):
|
|
||||||
queue_item = BookcaseItemBorrowingQueue(
|
|
||||||
item=books[4 + i],
|
|
||||||
username=f"test_queue_user_{i}",
|
|
||||||
)
|
|
||||||
sql_session.add(queue_item)
|
|
||||||
|
|
||||||
for i in range(3):
|
|
||||||
borrowing = BookcaseItemBorrowing(
|
|
||||||
item=books[4 + i],
|
|
||||||
username=f"test_borrower_returned_{i}",
|
|
||||||
)
|
|
||||||
borrowing.start_time = last_run_datetime - timedelta(days=2)
|
|
||||||
borrowing.end_time = datetime.now() + timedelta(days=1)
|
|
||||||
|
|
||||||
if i != 2:
|
|
||||||
borrowing.delivered = datetime.now() - timedelta(days=1)
|
|
||||||
|
|
||||||
sql_session.add(borrowing)
|
|
||||||
|
|
||||||
# Create at least one item that has been in the queue for so long that the queue position should expire
|
|
||||||
queue_item = BookcaseItemBorrowingQueue(
|
|
||||||
item=books[7],
|
|
||||||
username="test_queue_user_expired",
|
|
||||||
)
|
|
||||||
queue_item.entered_queue_time = datetime.now() - timedelta(days=15)
|
|
||||||
|
|
||||||
# Create at least one item that has been in the queue for so long that the queue position should expire,
|
|
||||||
# but the queue person has already been notified
|
|
||||||
queue_item = BookcaseItemBorrowingQueue(
|
|
||||||
item=books[8],
|
|
||||||
username="test_queue_user_expired_notified",
|
|
||||||
)
|
|
||||||
queue_item.entered_queue_time = datetime.now() - timedelta(days=15)
|
|
||||||
|
|
||||||
sql_session.commit()
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import csv
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from worblehat.models import (
|
|
||||||
Bookcase,
|
|
||||||
BookcaseItem,
|
|
||||||
BookcaseShelf,
|
|
||||||
MediaType,
|
|
||||||
Language,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
CSV_FILE = Path(__file__).parent.parent.parent.parent / "data" / "arbeidsrom_smal_hylle_5.csv"
|
|
||||||
|
|
||||||
|
|
||||||
def clear_db(sql_session):
|
|
||||||
sql_session.query(BookcaseItem).delete()
|
|
||||||
sql_session.query(BookcaseShelf).delete()
|
|
||||||
sql_session.query(Bookcase).delete()
|
|
||||||
sql_session.query(MediaType).delete()
|
|
||||||
sql_session.query(Language).delete()
|
|
||||||
sql_session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def main(sql_session):
|
|
||||||
clear_db(sql_session)
|
|
||||||
|
|
||||||
media_type = MediaType(
|
|
||||||
name="Book",
|
|
||||||
description="A book",
|
|
||||||
)
|
|
||||||
sql_session.add(media_type)
|
|
||||||
|
|
||||||
language = Language(
|
|
||||||
name="Norwegian",
|
|
||||||
iso639_1_code="no",
|
|
||||||
)
|
|
||||||
sql_session.add(language)
|
|
||||||
|
|
||||||
seed_case = Bookcase(
|
|
||||||
name="seed_case",
|
|
||||||
description="test bookcase with test data",
|
|
||||||
)
|
|
||||||
sql_session.add(seed_case)
|
|
||||||
|
|
||||||
seed_shelf_1 = BookcaseShelf(
|
|
||||||
row=1,
|
|
||||||
column=1,
|
|
||||||
bookcase=seed_case,
|
|
||||||
description="test shelf with test data 1",
|
|
||||||
)
|
|
||||||
seed_shelf_2 = BookcaseShelf(
|
|
||||||
row=2,
|
|
||||||
column=1,
|
|
||||||
bookcase=seed_case,
|
|
||||||
description="test shelf with test data 2",
|
|
||||||
)
|
|
||||||
sql_session.add(seed_shelf_1)
|
|
||||||
sql_session.add(seed_shelf_2)
|
|
||||||
|
|
||||||
bookcase_items = []
|
|
||||||
with open(CSV_FILE) as csv_file:
|
|
||||||
csv_reader = csv.reader(csv_file, delimiter=",")
|
|
||||||
|
|
||||||
next(csv_reader)
|
|
||||||
for row in csv_reader:
|
|
||||||
item = BookcaseItem(
|
|
||||||
isbn=int(row[0]),
|
|
||||||
name=row[1],
|
|
||||||
)
|
|
||||||
item.media_type = media_type
|
|
||||||
item.language = language
|
|
||||||
bookcase_items.append(item)
|
|
||||||
|
|
||||||
half = len(bookcase_items) // 2
|
|
||||||
first_half = bookcase_items[:half]
|
|
||||||
second_half = bookcase_items[half:]
|
|
||||||
|
|
||||||
for item in first_half:
|
|
||||||
seed_shelf_1.items.add(item)
|
|
||||||
|
|
||||||
for item in second_half:
|
|
||||||
seed_shelf_2.items.add(item)
|
|
||||||
|
|
||||||
sql_session.add_all(bookcase_items)
|
|
||||||
sql_session.commit()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from werkzeug import run_simple
|
|
||||||
|
|
||||||
|
|
||||||
from .flaskapp import create_app
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = create_app()
|
|
||||||
run_simple(
|
|
||||||
hostname="localhost",
|
|
||||||
port=5000,
|
|
||||||
application=app,
|
|
||||||
use_debugger=True,
|
|
||||||
use_reloader=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import logging
|
|
||||||
from pprint import pformat
|
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from .services import (
|
|
||||||
Config,
|
|
||||||
arg_parser,
|
|
||||||
devscripts_arg_parser,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .deadline_daemon import DeadlineDaemon
|
|
||||||
from .cli import WorblehatCli
|
|
||||||
from .flaskapp.wsgi_dev import main as flask_dev_main
|
|
||||||
from .flaskapp.wsgi_prod import main as flask_prod_main
|
|
||||||
|
|
||||||
|
|
||||||
def _print_version() -> None:
|
|
||||||
from importlib.metadata import version, PackageNotFoundError
|
|
||||||
|
|
||||||
try:
|
|
||||||
__version__ = version("worblehat")
|
|
||||||
except PackageNotFoundError:
|
|
||||||
__version__ = "unknown"
|
|
||||||
|
|
||||||
print(f"Worblehat version {__version__}")
|
|
||||||
|
|
||||||
|
|
||||||
def _connect_to_database(**engine_args) -> Session:
|
|
||||||
try:
|
|
||||||
engine = create_engine(Config.db_string(), **engine_args)
|
|
||||||
sql_session = Session(engine)
|
|
||||||
except Exception as err:
|
|
||||||
print("Error: could not connect to database.")
|
|
||||||
print(err)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
print(f"Debug: Connected to database at '{Config.db_string()}'")
|
|
||||||
return sql_session
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = arg_parser.parse_args()
|
|
||||||
Config.load_configuration(vars(args))
|
|
||||||
|
|
||||||
if Config["logging.debug"]:
|
|
||||||
logging.basicConfig(encoding="utf-8", level=logging.DEBUG)
|
|
||||||
else:
|
|
||||||
logging.basicConfig(encoding="utf-8", level=logging.INFO)
|
|
||||||
|
|
||||||
if args.version:
|
|
||||||
_print_version()
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
if args.print_config:
|
|
||||||
print(f"Configuration:\n{pformat(vars(args))}")
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
if args.command == "deadline-daemon":
|
|
||||||
sql_session = _connect_to_database(echo=Config["logging.debug_sql"])
|
|
||||||
DeadlineDaemon(sql_session).run()
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
if args.command == "cli":
|
|
||||||
sql_session = _connect_to_database(echo=Config["logging.debug_sql"])
|
|
||||||
WorblehatCli.run_with_safe_exit_wrapper(sql_session)
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
if args.command == "devscripts":
|
|
||||||
sql_session = _connect_to_database(echo=Config["logging.debug_sql"])
|
|
||||||
if args.script == "seed-content-for-deadline-daemon":
|
|
||||||
from .devscripts.seed_content_for_deadline_daemon import main
|
|
||||||
|
|
||||||
main(sql_session)
|
|
||||||
elif args.script == "seed-test-data":
|
|
||||||
from .devscripts.seed_test_data import main
|
|
||||||
|
|
||||||
main(sql_session)
|
|
||||||
else:
|
|
||||||
print(devscripts_arg_parser.format_help())
|
|
||||||
exit(1)
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
if args.command == "flask-dev":
|
|
||||||
flask_dev_main()
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
if args.command == "flask-prod":
|
|
||||||
if Config["logging.debug"] or Config["logging.debug_sql"]:
|
|
||||||
logging.warn(
|
|
||||||
"Debug mode is enabled for the production server. This is not recommended."
|
|
||||||
)
|
|
||||||
flask_prod_main()
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
print(arg_parser.format_help())
|
|
||||||
exit(1)
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
"""initial_migration
|
|
||||||
|
|
||||||
Revision ID: 7dfbf8a8dec8
|
|
||||||
Revises:
|
|
||||||
Create Date: 2024-07-31 21:07:13.434012
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "7dfbf8a8dec8"
|
|
||||||
down_revision = None
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table(
|
|
||||||
"Author",
|
|
||||||
sa.Column("uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("name", sa.Text(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_Author")),
|
|
||||||
)
|
|
||||||
with op.batch_alter_table("Author", schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f("ix_Author_name"), ["name"], unique=True)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"Bookcase",
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column("uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("name", sa.Text(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_Bookcase")),
|
|
||||||
)
|
|
||||||
with op.batch_alter_table("Bookcase", schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f("ix_Bookcase_name"), ["name"], unique=True)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"Category",
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column("uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("name", sa.Text(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_Category")),
|
|
||||||
)
|
|
||||||
with op.batch_alter_table("Category", schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f("ix_Category_name"), ["name"], unique=True)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"DeadlineDaemonLastRunDatetime",
|
|
||||||
sa.Column("uid", sa.Boolean(), nullable=False),
|
|
||||||
sa.Column("time", sa.DateTime(), nullable=False),
|
|
||||||
sa.CheckConstraint(
|
|
||||||
"uid = true",
|
|
||||||
name=op.f("ck_DeadlineDaemonLastRunDatetime_`single_row_only`"),
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_DeadlineDaemonLastRunDatetime")),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"Language",
|
|
||||||
sa.Column("iso639_1_code", sa.String(length=2), nullable=False),
|
|
||||||
sa.Column("uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("name", sa.Text(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_Language")),
|
|
||||||
)
|
|
||||||
with op.batch_alter_table("Language", schema=None) as batch_op:
|
|
||||||
batch_op.create_index(
|
|
||||||
batch_op.f("ix_Language_iso639_1_code"), ["iso639_1_code"], unique=True
|
|
||||||
)
|
|
||||||
batch_op.create_index(batch_op.f("ix_Language_name"), ["name"], unique=True)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"MediaType",
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column("uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("name", sa.Text(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_MediaType")),
|
|
||||||
)
|
|
||||||
with op.batch_alter_table("MediaType", schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f("ix_MediaType_name"), ["name"], unique=True)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"BookcaseShelf",
|
|
||||||
sa.Column("description", sa.Text(), nullable=True),
|
|
||||||
sa.Column("row", sa.SmallInteger(), nullable=False),
|
|
||||||
sa.Column("column", sa.SmallInteger(), nullable=False),
|
|
||||||
sa.Column("fk_bookcase_uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("uid", sa.Integer(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_bookcase_uid"],
|
|
||||||
["Bookcase.uid"],
|
|
||||||
name=op.f("fk_BookcaseShelf_fk_bookcase_uid_Bookcase"),
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_BookcaseShelf")),
|
|
||||||
sa.UniqueConstraint(
|
|
||||||
"column", "fk_bookcase_uid", "row", name=op.f("uq_BookcaseShelf_column")
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"BookcaseItem",
|
|
||||||
sa.Column("isbn", sa.String(), nullable=False),
|
|
||||||
sa.Column("name", sa.Text(), nullable=False),
|
|
||||||
sa.Column("owner", sa.String(), nullable=False),
|
|
||||||
sa.Column("amount", sa.SmallInteger(), nullable=False),
|
|
||||||
sa.Column("fk_media_type_uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("fk_bookcase_shelf_uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("fk_language_uid", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("uid", sa.Integer(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_bookcase_shelf_uid"],
|
|
||||||
["BookcaseShelf.uid"],
|
|
||||||
name=op.f("fk_BookcaseItem_fk_bookcase_shelf_uid_BookcaseShelf"),
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_language_uid"],
|
|
||||||
["Language.uid"],
|
|
||||||
name=op.f("fk_BookcaseItem_fk_language_uid_Language"),
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_media_type_uid"],
|
|
||||||
["MediaType.uid"],
|
|
||||||
name=op.f("fk_BookcaseItem_fk_media_type_uid_MediaType"),
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_BookcaseItem")),
|
|
||||||
)
|
|
||||||
with op.batch_alter_table("BookcaseItem", schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f("ix_BookcaseItem_isbn"), ["isbn"], unique=True)
|
|
||||||
batch_op.create_index(
|
|
||||||
batch_op.f("ix_BookcaseItem_name"), ["name"], unique=False
|
|
||||||
)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"BookcaseItemBorrowing",
|
|
||||||
sa.Column("username", sa.String(), nullable=False),
|
|
||||||
sa.Column("start_time", sa.DateTime(), nullable=False),
|
|
||||||
sa.Column("end_time", sa.DateTime(), nullable=False),
|
|
||||||
sa.Column("delivered", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("fk_bookcase_item_uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("uid", sa.Integer(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_bookcase_item_uid"],
|
|
||||||
["BookcaseItem.uid"],
|
|
||||||
name=op.f("fk_BookcaseItemBorrowing_fk_bookcase_item_uid_BookcaseItem"),
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_BookcaseItemBorrowing")),
|
|
||||||
)
|
|
||||||
with op.batch_alter_table("BookcaseItemBorrowing", schema=None) as batch_op:
|
|
||||||
batch_op.create_index(
|
|
||||||
batch_op.f("ix_BookcaseItemBorrowing_fk_bookcase_item_uid"),
|
|
||||||
["fk_bookcase_item_uid"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"BookcaseItemBorrowingQueue",
|
|
||||||
sa.Column("username", sa.String(), nullable=False),
|
|
||||||
sa.Column("entered_queue_time", sa.DateTime(), nullable=False),
|
|
||||||
sa.Column("item_became_available_time", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("expired", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("fk_bookcase_item_uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("uid", sa.Integer(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_bookcase_item_uid"],
|
|
||||||
["BookcaseItem.uid"],
|
|
||||||
name=op.f(
|
|
||||||
"fk_BookcaseItemBorrowingQueue_fk_bookcase_item_uid_BookcaseItem"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("uid", name=op.f("pk_BookcaseItemBorrowingQueue")),
|
|
||||||
)
|
|
||||||
with op.batch_alter_table("BookcaseItemBorrowingQueue", schema=None) as batch_op:
|
|
||||||
batch_op.create_index(
|
|
||||||
batch_op.f("ix_BookcaseItemBorrowingQueue_fk_bookcase_item_uid"),
|
|
||||||
["fk_bookcase_item_uid"],
|
|
||||||
unique=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
op.create_table(
|
|
||||||
"Item_Author",
|
|
||||||
sa.Column("fk_item_uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("fk_author_uid", sa.Integer(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_author_uid"],
|
|
||||||
["Author.uid"],
|
|
||||||
name=op.f("fk_Item_Author_fk_author_uid_Author"),
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_item_uid"],
|
|
||||||
["BookcaseItem.uid"],
|
|
||||||
name=op.f("fk_Item_Author_fk_item_uid_BookcaseItem"),
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint(
|
|
||||||
"fk_item_uid", "fk_author_uid", name=op.f("pk_Item_Author")
|
|
||||||
),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"Item_Category",
|
|
||||||
sa.Column("fk_item_uid", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("fk_category_uid", sa.Integer(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_category_uid"],
|
|
||||||
["Category.uid"],
|
|
||||||
name=op.f("fk_Item_Category_fk_category_uid_Category"),
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["fk_item_uid"],
|
|
||||||
["BookcaseItem.uid"],
|
|
||||||
name=op.f("fk_Item_Category_fk_item_uid_BookcaseItem"),
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint(
|
|
||||||
"fk_item_uid", "fk_category_uid", name=op.f("pk_Item_Category")
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table("Item_Category")
|
|
||||||
op.drop_table("Item_Author")
|
|
||||||
with op.batch_alter_table("BookcaseItemBorrowingQueue", schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(
|
|
||||||
batch_op.f("ix_BookcaseItemBorrowingQueue_fk_bookcase_item_uid")
|
|
||||||
)
|
|
||||||
|
|
||||||
op.drop_table("BookcaseItemBorrowingQueue")
|
|
||||||
with op.batch_alter_table("BookcaseItemBorrowing", schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f("ix_BookcaseItemBorrowing_fk_bookcase_item_uid"))
|
|
||||||
|
|
||||||
op.drop_table("BookcaseItemBorrowing")
|
|
||||||
with op.batch_alter_table("BookcaseItem", schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f("ix_BookcaseItem_name"))
|
|
||||||
batch_op.drop_index(batch_op.f("ix_BookcaseItem_isbn"))
|
|
||||||
|
|
||||||
op.drop_table("BookcaseItem")
|
|
||||||
op.drop_table("BookcaseShelf")
|
|
||||||
with op.batch_alter_table("MediaType", schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f("ix_MediaType_name"))
|
|
||||||
|
|
||||||
op.drop_table("MediaType")
|
|
||||||
with op.batch_alter_table("Language", schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f("ix_Language_name"))
|
|
||||||
batch_op.drop_index(batch_op.f("ix_Language_iso639_1_code"))
|
|
||||||
|
|
||||||
op.drop_table("Language")
|
|
||||||
op.drop_table("DeadlineDaemonLastRunDatetime")
|
|
||||||
with op.batch_alter_table("Category", schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f("ix_Category_name"))
|
|
||||||
|
|
||||||
op.drop_table("Category")
|
|
||||||
with op.batch_alter_table("Bookcase", schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f("ix_Bookcase_name"))
|
|
||||||
|
|
||||||
op.drop_table("Bookcase")
|
|
||||||
with op.batch_alter_table("Author", schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f("ix_Author_name"))
|
|
||||||
|
|
||||||
op.drop_table("Author")
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from sqlalchemy import (
|
|
||||||
ForeignKey,
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import (
|
|
||||||
Mapped,
|
|
||||||
mapped_column,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ..Base import Base
|
|
||||||
from ..mixins.XrefMixin import XrefMixin
|
|
||||||
|
|
||||||
|
|
||||||
class Item_Author(Base, XrefMixin):
|
|
||||||
fk_item_uid: Mapped[int] = mapped_column(
|
|
||||||
ForeignKey("BookcaseItem.uid"), primary_key=True
|
|
||||||
)
|
|
||||||
fk_author_uid: Mapped[int] = mapped_column(
|
|
||||||
ForeignKey("Author.uid"), primary_key=True
|
|
||||||
)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from sqlalchemy import (
|
|
||||||
ForeignKey,
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import (
|
|
||||||
Mapped,
|
|
||||||
mapped_column,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ..Base import Base
|
|
||||||
from ..mixins.XrefMixin import XrefMixin
|
|
||||||
|
|
||||||
|
|
||||||
class Item_Category(Base, XrefMixin):
|
|
||||||
fk_item_uid: Mapped[int] = mapped_column(
|
|
||||||
ForeignKey("BookcaseItem.uid"), primary_key=True
|
|
||||||
)
|
|
||||||
fk_category_uid: Mapped[int] = mapped_column(
|
|
||||||
ForeignKey("Category.uid"), primary_key=True
|
|
||||||
)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .Item_Author import Item_Author
|
|
||||||
from .Item_Category import Item_Category
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"Item_Author",
|
|
||||||
"Item_Category",
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from .argument_parser import (
|
|
||||||
arg_parser,
|
|
||||||
devscripts_arg_parser,
|
|
||||||
)
|
|
||||||
from .bookcase_item import (
|
|
||||||
create_bookcase_item_from_isbn,
|
|
||||||
is_valid_isbn,
|
|
||||||
)
|
|
||||||
from .config import Config
|
|
||||||
from .email import send_email
|
|
||||||
from .seed_test_data import seed_data
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"arg_parser",
|
|
||||||
"devscripts_arg_parser",
|
|
||||||
"Config",
|
|
||||||
"create_bookcase_item_from_isbn",
|
|
||||||
"is_valid_isbn",
|
|
||||||
"send_email",
|
|
||||||
"seed_data",
|
|
||||||
]
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
from argparse import ArgumentParser
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_file(parser: ArgumentParser, arg: str) -> Path:
|
|
||||||
path = Path(arg)
|
|
||||||
if not path.is_file():
|
|
||||||
parser.error(f"The file {arg} does not exist!")
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
arg_parser = ArgumentParser(
|
|
||||||
description="Worblehat library management system",
|
|
||||||
)
|
|
||||||
|
|
||||||
subparsers = arg_parser.add_subparsers(dest="command")
|
|
||||||
subparsers.add_parser(
|
|
||||||
"deadline-daemon",
|
|
||||||
help="Initialize a single pass of the daemon which sends deadline emails",
|
|
||||||
)
|
|
||||||
subparsers.add_parser(
|
|
||||||
"cli",
|
|
||||||
help="Start the command line interface",
|
|
||||||
)
|
|
||||||
subparsers.add_parser(
|
|
||||||
"flask-dev",
|
|
||||||
help="Start the web interface in development mode",
|
|
||||||
)
|
|
||||||
subparsers.add_parser(
|
|
||||||
"flask-prod",
|
|
||||||
help="Start the web interface in production mode",
|
|
||||||
)
|
|
||||||
|
|
||||||
devscripts_arg_parser = subparsers.add_parser(
|
|
||||||
"devscripts", help="Run development scripts"
|
|
||||||
)
|
|
||||||
devscripts_subparsers = devscripts_arg_parser.add_subparsers(dest="script")
|
|
||||||
|
|
||||||
devscripts_subparsers.add_parser(
|
|
||||||
"seed-test-data",
|
|
||||||
help="Seed test data in the database",
|
|
||||||
)
|
|
||||||
|
|
||||||
devscripts_subparsers.add_parser(
|
|
||||||
"seed-content-for-deadline-daemon",
|
|
||||||
help="Seed data tailorded for testing the deadline daemon, into the database",
|
|
||||||
)
|
|
||||||
|
|
||||||
arg_parser.add_argument(
|
|
||||||
"-V",
|
|
||||||
"--version",
|
|
||||||
action="store_true",
|
|
||||||
help="Print version and exit",
|
|
||||||
)
|
|
||||||
arg_parser.add_argument(
|
|
||||||
"-c",
|
|
||||||
"--config",
|
|
||||||
type=lambda x: _is_valid_file(arg_parser, x),
|
|
||||||
help="Path to config file",
|
|
||||||
dest="config_file",
|
|
||||||
metavar="FILE",
|
|
||||||
)
|
|
||||||
arg_parser.add_argument(
|
|
||||||
"-p",
|
|
||||||
"--print-config",
|
|
||||||
action="store_true",
|
|
||||||
help="Print configuration and quit",
|
|
||||||
)
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import isbnlib
|
|
||||||
|
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from .metadata_fetchers import fetch_metadata_from_multiple_sources
|
|
||||||
from ..models import (
|
|
||||||
Author,
|
|
||||||
BookcaseItem,
|
|
||||||
Language,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_pvv_isbn(isbn: str) -> bool:
|
|
||||||
try:
|
|
||||||
int(isbn)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
return len(isbn) == 8
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_isbn(isbn: str) -> bool:
|
|
||||||
return any(
|
|
||||||
[
|
|
||||||
isbnlib.is_isbn10(isbn),
|
|
||||||
isbnlib.is_isbn13(isbn),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_bookcase_item_from_isbn(
|
|
||||||
isbn: str,
|
|
||||||
sql_session: Session,
|
|
||||||
) -> BookcaseItem | None:
|
|
||||||
"""
|
|
||||||
This function fetches metadata for the given ISBN and creates a BookcaseItem from it.
|
|
||||||
It does so using a database connection to connect it to the correct authors and language
|
|
||||||
through the sql ORM.
|
|
||||||
|
|
||||||
If no metadata is found, None is returned.
|
|
||||||
|
|
||||||
Please not that the returned BookcaseItem will likely not be fully populated with the required
|
|
||||||
data, such as the book's location in the library, and the owner of the book, etc.
|
|
||||||
"""
|
|
||||||
metadata = fetch_metadata_from_multiple_sources(isbn)
|
|
||||||
if len(metadata) == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
metadata = metadata[0]
|
|
||||||
|
|
||||||
bookcase_item = BookcaseItem(
|
|
||||||
name=metadata.title,
|
|
||||||
isbn=int(isbn.replace("-", "")),
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(authors := metadata.authors) > 0:
|
|
||||||
for author in authors:
|
|
||||||
bookcase_item.authors.add(Author(author))
|
|
||||||
|
|
||||||
if language := metadata.language:
|
|
||||||
bookcase_item.language = sql_session.scalars(
|
|
||||||
select(Language).where(Language.iso639_1_code == language)
|
|
||||||
).one()
|
|
||||||
|
|
||||||
return bookcase_item
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import smtplib
|
|
||||||
|
|
||||||
from textwrap import indent
|
|
||||||
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
|
||||||
|
|
||||||
from .config import Config
|
|
||||||
|
|
||||||
|
|
||||||
def send_email(to: str, subject: str, body: str):
|
|
||||||
msg = MIMEMultipart()
|
|
||||||
msg["From"] = Config["smtp.from"]
|
|
||||||
msg["To"] = to
|
|
||||||
if Config["smtp.subject_prefix"]:
|
|
||||||
msg["Subject"] = f"{Config['smtp.subject_prefix']} {subject}"
|
|
||||||
else:
|
|
||||||
msg["Subject"] = subject
|
|
||||||
msg.attach(MIMEText(body, "plain"))
|
|
||||||
|
|
||||||
if Config["smtp.enabled"] and not Config["deadline_daemon.dryrun"]:
|
|
||||||
try:
|
|
||||||
with smtplib.SMTP(Config["smtp.host"], Config["smtp.port"]) as server:
|
|
||||||
server.starttls()
|
|
||||||
server.login(
|
|
||||||
Config["smtp.username"],
|
|
||||||
Config.read_password(Config["smtp.password"]),
|
|
||||||
)
|
|
||||||
server.sendmail(Config["smtp.from"], to, msg.as_string())
|
|
||||||
except Exception as err:
|
|
||||||
print("Error: could not send email.")
|
|
||||||
print(err)
|
|
||||||
else:
|
|
||||||
print("Debug: Email sending is disabled, so the following email was not sent:")
|
|
||||||
print(indent(msg.as_string(), " "))
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from typing import Set
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Add more languages
|
|
||||||
LANGUAGES: set[str] = set(
|
|
||||||
[
|
|
||||||
"no",
|
|
||||||
"en",
|
|
||||||
"de",
|
|
||||||
"fr",
|
|
||||||
"es",
|
|
||||||
"it",
|
|
||||||
"sv",
|
|
||||||
"da",
|
|
||||||
"fi",
|
|
||||||
"ru",
|
|
||||||
"zh",
|
|
||||||
"ja",
|
|
||||||
"ko",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BookMetadata:
|
|
||||||
"""A class representing metadata for a book."""
|
|
||||||
|
|
||||||
isbn: str
|
|
||||||
title: str
|
|
||||||
# The source of the metadata provider
|
|
||||||
source: str
|
|
||||||
authors: Set[str]
|
|
||||||
language: str | None
|
|
||||||
publish_date: str | None
|
|
||||||
num_pages: int | None
|
|
||||||
subjects: Set[str]
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, any]:
|
|
||||||
return {
|
|
||||||
"isbn": self.isbn,
|
|
||||||
"title": self.title,
|
|
||||||
"source": self.metadata_source_id(),
|
|
||||||
"authors": set() if self.authors is None else self.authors,
|
|
||||||
"language": self.language,
|
|
||||||
"publish_date": self.publish_date,
|
|
||||||
"num_pages": self.num_pages,
|
|
||||||
"subjects": set() if self.subjects is None else self.subjects,
|
|
||||||
}
|
|
||||||
|
|
||||||
def validate(self) -> None:
|
|
||||||
if not self.isbn:
|
|
||||||
raise ValueError("Missing ISBN")
|
|
||||||
if not self.title:
|
|
||||||
raise ValueError("Missing title")
|
|
||||||
if not self.source:
|
|
||||||
raise ValueError("Missing source")
|
|
||||||
if not self.authors:
|
|
||||||
raise ValueError("Missing authors")
|
|
||||||
|
|
||||||
if self.language is not None and self.language not in LANGUAGES:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid language: {self.language}. Consider adding it to the LANGUAGES set if you think this is a mistake."
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.num_pages is not None and self.num_pages < 0:
|
|
||||||
raise ValueError(f"Invalid number of pages: {self.num_pages}")
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# base fetcher.
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from .BookMetadata import BookMetadata
|
|
||||||
|
|
||||||
|
|
||||||
class BookMetadataFetcher(ABC):
|
|
||||||
"""
|
|
||||||
A base class for metadata fetchers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abstractmethod
|
|
||||||
def metadata_source_id(cls) -> str:
|
|
||||||
"""Returns a unique identifier for the metadata source, to identify where the metadata came from."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abstractmethod
|
|
||||||
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
|
||||||
"""Tries to fetch metadata for the given ISBN."""
|
|
||||||
pass
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"""
|
|
||||||
A BookMetadataFetcher for the Google Books API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
|
|
||||||
|
|
||||||
class GoogleBooksFetcher(BookMetadataFetcher):
|
|
||||||
@classmethod
|
|
||||||
def metadata_source_id(_cls) -> str:
|
|
||||||
return "google_books"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
|
||||||
try:
|
|
||||||
jsonInput = requests.get(
|
|
||||||
"https://www.googleapis.com/books/v1/volumes",
|
|
||||||
params={"q": f"isbn:{isbn}"},
|
|
||||||
).json()
|
|
||||||
data = jsonInput.get("items")[0].get("volumeInfo")
|
|
||||||
|
|
||||||
authors = set(data.get("authors") or [])
|
|
||||||
title = data.get("title")
|
|
||||||
publishDate = data.get("publish_date")
|
|
||||||
numberOfPages = data.get("number_of_pages")
|
|
||||||
if numberOfPages:
|
|
||||||
numberOfPages = int(numberOfPages)
|
|
||||||
subjects = set(data.get("categories") or [])
|
|
||||||
languages = data.get("languages")
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return BookMetadata(
|
|
||||||
isbn=isbn,
|
|
||||||
title=title,
|
|
||||||
source=cls.metadata_source_id(),
|
|
||||||
authors=authors,
|
|
||||||
language=languages,
|
|
||||||
publish_date=publishDate,
|
|
||||||
num_pages=numberOfPages,
|
|
||||||
subjects=subjects,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
book_data = GoogleBooksFetcher.fetch_metadata("0132624788")
|
|
||||||
book_data.validate()
|
|
||||||
print(book_data)
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
"""
|
|
||||||
A BookMetadataFetcher for the Open Library API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
|
|
||||||
LANGUAGE_MAP = {
|
|
||||||
"Norwegian": "no",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class OpenLibraryFetcher(BookMetadataFetcher):
|
|
||||||
@classmethod
|
|
||||||
def metadata_source_id(_cls) -> str:
|
|
||||||
return "open_library"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
|
||||||
try:
|
|
||||||
jsonInput = requests.get(f"https://openlibrary.org/isbn/{isbn}.json").json()
|
|
||||||
|
|
||||||
author_keys = jsonInput.get("authors") or []
|
|
||||||
author_names = set()
|
|
||||||
for author_key in author_keys:
|
|
||||||
key = author_key.get("key")
|
|
||||||
author_name = (
|
|
||||||
requests.get(f"https://openlibrary.org/{key}.json")
|
|
||||||
.json()
|
|
||||||
.get("name")
|
|
||||||
)
|
|
||||||
author_names.add(author_name)
|
|
||||||
|
|
||||||
title = jsonInput.get("title")
|
|
||||||
publishDate = jsonInput.get("publish_date")
|
|
||||||
|
|
||||||
numberOfPages = jsonInput.get("number_of_pages")
|
|
||||||
if numberOfPages:
|
|
||||||
numberOfPages = int(numberOfPages)
|
|
||||||
|
|
||||||
language_key = jsonInput.get("languages")[0].get("key")
|
|
||||||
language = (
|
|
||||||
requests.get(f"https://openlibrary.org/{language_key}.json")
|
|
||||||
.json()
|
|
||||||
.get("identifiers")
|
|
||||||
.get("iso_639_1")[0]
|
|
||||||
)
|
|
||||||
subjects = set(jsonInput.get("subjects") or [])
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return BookMetadata(
|
|
||||||
isbn=isbn,
|
|
||||||
title=title,
|
|
||||||
source=cls.metadata_source_id(),
|
|
||||||
authors=author_names,
|
|
||||||
language=language,
|
|
||||||
publish_date=publishDate,
|
|
||||||
num_pages=numberOfPages,
|
|
||||||
subjects=subjects,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
book_data = OpenLibraryFetcher.fetch_metadata("9788205530751")
|
|
||||||
book_data.validate()
|
|
||||||
print(book_data)
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
"""
|
|
||||||
A BookMetadataFetcher that webscrapes https://outland.no/
|
|
||||||
"""
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
|
|
||||||
|
|
||||||
LANGUAGE_MAP = {
|
|
||||||
"Norsk": "no",
|
|
||||||
"Engelsk": "en",
|
|
||||||
"Tysk": "de",
|
|
||||||
"Fransk": "fr",
|
|
||||||
"Spansk": "es",
|
|
||||||
"Italiensk": "it",
|
|
||||||
"Svensk": "sv",
|
|
||||||
"Dansk": "da",
|
|
||||||
"Finsk": "fi",
|
|
||||||
"Russisk": "ru",
|
|
||||||
"Kinesisk": "zh",
|
|
||||||
"Japansk": "ja",
|
|
||||||
"Koreansk": "ko",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class OutlandScraperFetcher(BookMetadataFetcher):
|
|
||||||
@classmethod
|
|
||||||
def metadata_source_id(_cls) -> str:
|
|
||||||
return "outland_scraper"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
|
||||||
try:
|
|
||||||
# Find the link to the product page
|
|
||||||
response = requests.get(f"https://outland.no/{isbn}")
|
|
||||||
soup = BeautifulSoup(response.content, "html.parser")
|
|
||||||
soup = soup.find_all("a", class_="product-item-link")
|
|
||||||
href = soup[0].get("href")
|
|
||||||
|
|
||||||
# Find the metadata on the product page
|
|
||||||
response = requests.get(href)
|
|
||||||
soup = BeautifulSoup(response.content, "html.parser")
|
|
||||||
data = soup.find_all("td", class_="col data")
|
|
||||||
|
|
||||||
# Collect the metadata
|
|
||||||
title = soup.find_all("span", class_="base")[0].text
|
|
||||||
|
|
||||||
releaseDate = soup.find_all("span", class_="release-date")[0].text.strip()
|
|
||||||
releaseDate = releaseDate[-4:] # only keep year
|
|
||||||
|
|
||||||
bookData = {
|
|
||||||
"Title": title,
|
|
||||||
"PublishDate": releaseDate,
|
|
||||||
"Authors": None,
|
|
||||||
"NumberOfPages": None,
|
|
||||||
"Genre": None,
|
|
||||||
"Language": None,
|
|
||||||
"Subjects": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
dataKeyMap = {
|
|
||||||
"Authors": "Forfattere",
|
|
||||||
"NumberOfPages": "Antall Sider",
|
|
||||||
"Genre": "Sjanger",
|
|
||||||
"Language": "Språk",
|
|
||||||
"Subjects": "Serie",
|
|
||||||
}
|
|
||||||
|
|
||||||
for value in data:
|
|
||||||
for key in dataKeyMap:
|
|
||||||
if str(value).lower().__contains__(dataKeyMap[key].lower()):
|
|
||||||
bookData[key] = value.text
|
|
||||||
break
|
|
||||||
|
|
||||||
if bookData["Language"] is not None:
|
|
||||||
bookData["Language"] = LANGUAGE_MAP.get(bookData["Language"])
|
|
||||||
|
|
||||||
if bookData["Authors"] is not None:
|
|
||||||
bookData["Authors"] = set(bookData["Authors"].split(", "))
|
|
||||||
|
|
||||||
if bookData["Subjects"] is not None:
|
|
||||||
bookData["Subjects"] = set(bookData["Subjects"].split(", "))
|
|
||||||
|
|
||||||
if bookData["NumberOfPages"] is not None:
|
|
||||||
bookData["NumberOfPages"] = int(bookData["NumberOfPages"])
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return BookMetadata(
|
|
||||||
isbn=isbn,
|
|
||||||
title=bookData.get("Title"),
|
|
||||||
source=cls.metadata_source_id(),
|
|
||||||
authors=bookData.get("Authors"),
|
|
||||||
language=bookData.get("Language"),
|
|
||||||
publish_date=bookData.get("PublishDate"),
|
|
||||||
num_pages=bookData.get("NumberOfPages"),
|
|
||||||
subjects=bookData.get("Subjects"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
book_data = OutlandScraperFetcher.fetch_metadata("9781947808225")
|
|
||||||
book_data.validate()
|
|
||||||
print(book_data)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .book_metadata_fetcher import fetch_metadata_from_multiple_sources
|
|
||||||
|
|
||||||
__all__ = ["fetch_metadata_from_multiple_sources"]
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
"""
|
|
||||||
this module contains the fetch_book_metadata() function which fetches book metadata from multiple sources in threads and returns the higest ranked non-None result.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.GoogleBooksFetcher import GoogleBooksFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.OpenLibraryFetcher import OpenLibraryFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.OutlandScraperFetcher import (
|
|
||||||
OutlandScraperFetcher,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# The order of these fetchers determines the priority of the sources.
|
|
||||||
# The first fetcher in the list has the highest priority.
|
|
||||||
FETCHERS: list[BookMetadataFetcher] = [
|
|
||||||
OpenLibraryFetcher,
|
|
||||||
GoogleBooksFetcher,
|
|
||||||
OutlandScraperFetcher,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
FETCHER_SOURCE_IDS: list[str] = [fetcher.metadata_source_id() for fetcher in FETCHERS]
|
|
||||||
|
|
||||||
|
|
||||||
def sort_metadata_by_priority(metadata: list[BookMetadata]) -> list[BookMetadata]:
|
|
||||||
"""
|
|
||||||
Sorts the given metadata by the priority of the sources.
|
|
||||||
|
|
||||||
The order of the metadata is the same as the order of the sources in the FETCHERS list.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Note that this function is O(n^2) but the number of fetchers is small so it's fine.
|
|
||||||
return sorted(metadata, key=lambda m: FETCHER_SOURCE_IDS.index(m.source))
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_metadata_from_multiple_sources(isbn: str, strict=False) -> list[BookMetadata]:
|
|
||||||
"""
|
|
||||||
Returns a list of metadata fetched from multiple sources.
|
|
||||||
|
|
||||||
Sources that does not have metadata for the given ISBN will be ignored.
|
|
||||||
|
|
||||||
There is no guarantee that there will be any metadata.
|
|
||||||
|
|
||||||
The results are always ordered in the same way as the fetchers are listed in the FETCHERS list.
|
|
||||||
"""
|
|
||||||
isbn = isbn.replace("-", "").replace("_", "").strip().lower()
|
|
||||||
if len(isbn) != 10 and len(isbn) != 13 and not isbn.isnumeric():
|
|
||||||
raise ValueError("Invalid ISBN")
|
|
||||||
|
|
||||||
results: list[BookMetadata] = []
|
|
||||||
|
|
||||||
with ThreadPoolExecutor() as executor:
|
|
||||||
futures = [
|
|
||||||
executor.submit(fetcher.fetch_metadata, isbn) for fetcher in FETCHERS
|
|
||||||
]
|
|
||||||
|
|
||||||
for future in futures:
|
|
||||||
result = future.result()
|
|
||||||
if result is not None:
|
|
||||||
results.append(result)
|
|
||||||
|
|
||||||
for result in results:
|
|
||||||
try:
|
|
||||||
result.validate()
|
|
||||||
except ValueError as e:
|
|
||||||
if strict:
|
|
||||||
raise e
|
|
||||||
else:
|
|
||||||
print(f"Invalid metadata: {e}")
|
|
||||||
results.remove(result)
|
|
||||||
|
|
||||||
return sort_metadata_by_priority(results)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
isbn = "0132624788"
|
|
||||||
metadata = fetch_metadata_from_multiple_sources(isbn)
|
|
||||||
pprint(metadata)
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
import csv
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from worblehat.flaskapp.database import db
|
|
||||||
|
|
||||||
from ..models import (
|
|
||||||
Author,
|
|
||||||
Bookcase,
|
|
||||||
BookcaseItem,
|
|
||||||
BookcaseItemBorrowing,
|
|
||||||
BookcaseItemBorrowingQueue,
|
|
||||||
BookcaseShelf,
|
|
||||||
Language,
|
|
||||||
MediaType,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def seed_data(sql_session: Session = db.session):
|
|
||||||
media_types = [
|
|
||||||
MediaType(name="Book", description="A physical book"),
|
|
||||||
MediaType(name="Comic", description="A comic book"),
|
|
||||||
MediaType(
|
|
||||||
name="Video Game",
|
|
||||||
description="A digital game for computers or games consoles",
|
|
||||||
),
|
|
||||||
MediaType(
|
|
||||||
name="Tabletop Game",
|
|
||||||
description="A physical game with cards, boards or similar",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
bookcases = [
|
|
||||||
Bookcase(name="Unnamed A", description="White case across dibbler"),
|
|
||||||
Bookcase(name="Unnamed B", description="Math case in the working room"),
|
|
||||||
Bookcase(name="Unnamed C", description="Large case in the working room"),
|
|
||||||
Bookcase(name="Unnamed D", description="White comics case in the hallway"),
|
|
||||||
Bookcase(name="Unnamed E", description="Wooden comics case in the hallway"),
|
|
||||||
]
|
|
||||||
|
|
||||||
shelfs = [
|
|
||||||
BookcaseShelf(row=0, column=0, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=1, column=0, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=2, column=0, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=3, column=0, bookcase=bookcases[0], description="Hacking"),
|
|
||||||
BookcaseShelf(row=4, column=0, bookcase=bookcases[0], description="Hacking"),
|
|
||||||
BookcaseShelf(row=0, column=1, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=1, column=1, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=2, column=1, bookcase=bookcases[0], description="DOS"),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=3, column=1, bookcase=bookcases[0], description="Food for thought"
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=4, column=1, bookcase=bookcases[0], description="CPP"),
|
|
||||||
BookcaseShelf(row=0, column=2, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=1, column=2, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=2, column=2, bookcase=bookcases[0], description="E = mc2"),
|
|
||||||
BookcaseShelf(row=3, column=2, bookcase=bookcases[0], description="OBJECTION!"),
|
|
||||||
BookcaseShelf(row=4, column=2, bookcase=bookcases[0], description="/home"),
|
|
||||||
BookcaseShelf(row=0, column=3, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=1, column=3, bookcase=bookcases[0], description="Big indonisian island"
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=2, column=3, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=3, column=3, bookcase=bookcases[0], description="Div science"
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=4, column=3, bookcase=bookcases[0], description="/home"),
|
|
||||||
BookcaseShelf(row=0, column=4, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=1, column=4, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=2, column=4, bookcase=bookcases[0], description="(not) computer vision"
|
|
||||||
),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=3, column=4, bookcase=bookcases[0], description="Low voltage"
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=4, column=4, bookcase=bookcases[0], description="/home"),
|
|
||||||
BookcaseShelf(row=0, column=5, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=1, column=5, bookcase=bookcases[0]),
|
|
||||||
BookcaseShelf(row=2, column=5, bookcase=bookcases[0], description="/home"),
|
|
||||||
BookcaseShelf(row=3, column=5, bookcase=bookcases[0], description="/home"),
|
|
||||||
BookcaseShelf(row=0, column=0, bookcase=bookcases[1]),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=1,
|
|
||||||
column=0,
|
|
||||||
bookcase=bookcases[1],
|
|
||||||
description="Kjellerarealer og komodovaraner",
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=2, column=0, bookcase=bookcases[1]),
|
|
||||||
BookcaseShelf(row=3, column=0, bookcase=bookcases[1], description="Quick mafs"),
|
|
||||||
BookcaseShelf(row=4, column=0, bookcase=bookcases[1]),
|
|
||||||
BookcaseShelf(row=0, column=0, bookcase=bookcases[2]),
|
|
||||||
BookcaseShelf(row=1, column=0, bookcase=bookcases[2]),
|
|
||||||
BookcaseShelf(row=2, column=0, bookcase=bookcases[2], description="AI"),
|
|
||||||
BookcaseShelf(row=3, column=0, bookcase=bookcases[2], description="X86"),
|
|
||||||
BookcaseShelf(row=4, column=0, bookcase=bookcases[2], description="Humanoira"),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=5,
|
|
||||||
column=0,
|
|
||||||
bookcase=bookcases[2],
|
|
||||||
description="Hvem monterte rørforsterker?",
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=0, column=1, bookcase=bookcases[2]),
|
|
||||||
BookcaseShelf(row=1, column=1, bookcase=bookcases[2], description="Div data"),
|
|
||||||
BookcaseShelf(row=2, column=1, bookcase=bookcases[2], description="Chemistry"),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=3,
|
|
||||||
column=1,
|
|
||||||
bookcase=bookcases[2],
|
|
||||||
description="Soviet Phys. Techn. Phys",
|
|
||||||
),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=4, column=1, bookcase=bookcases[2], description="Digitalteknikk"
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=5, column=1, bookcase=bookcases[2], description="Material"),
|
|
||||||
BookcaseShelf(row=0, column=2, bookcase=bookcases[2]),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=1, column=2, bookcase=bookcases[2], description="Assembler / APL"
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=2, column=2, bookcase=bookcases[2], description="Internet"),
|
|
||||||
BookcaseShelf(row=3, column=2, bookcase=bookcases[2], description="Algorithms"),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=4, column=2, bookcase=bookcases[2], description="Soviet Physics Jetp"
|
|
||||||
),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=5, column=2, bookcase=bookcases[2], description="Død og pine"
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=0, column=3, bookcase=bookcases[2]),
|
|
||||||
BookcaseShelf(row=1, column=3, bookcase=bookcases[2], description="Web"),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=2, column=3, bookcase=bookcases[2], description="Div languages"
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=3, column=3, bookcase=bookcases[2], description="Python"),
|
|
||||||
BookcaseShelf(row=4, column=3, bookcase=bookcases[2], description="D&D Minis"),
|
|
||||||
BookcaseShelf(row=5, column=3, bookcase=bookcases[2], description="Perl"),
|
|
||||||
BookcaseShelf(row=0, column=4, bookcase=bookcases[2]),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=1, column=4, bookcase=bookcases[2], description="Knuth on programming"
|
|
||||||
),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=2, column=4, bookcase=bookcases[2], description="Div languages"
|
|
||||||
),
|
|
||||||
BookcaseShelf(
|
|
||||||
row=3, column=4, bookcase=bookcases[2], description="Typesetting"
|
|
||||||
),
|
|
||||||
BookcaseShelf(row=4, column=4, bookcase=bookcases[2]),
|
|
||||||
BookcaseShelf(row=0, column=0, bookcase=bookcases[3]),
|
|
||||||
BookcaseShelf(row=0, column=1, bookcase=bookcases[3]),
|
|
||||||
BookcaseShelf(row=0, column=2, bookcase=bookcases[3]),
|
|
||||||
BookcaseShelf(row=0, column=3, bookcase=bookcases[3]),
|
|
||||||
BookcaseShelf(row=0, column=4, bookcase=bookcases[3]),
|
|
||||||
BookcaseShelf(row=0, column=0, bookcase=bookcases[4]),
|
|
||||||
BookcaseShelf(row=0, column=1, bookcase=bookcases[4]),
|
|
||||||
BookcaseShelf(row=0, column=2, bookcase=bookcases[4]),
|
|
||||||
BookcaseShelf(row=0, column=3, bookcase=bookcases[4]),
|
|
||||||
BookcaseShelf(row=0, column=4, bookcase=bookcases[4], description="Religion"),
|
|
||||||
]
|
|
||||||
|
|
||||||
authors = [
|
|
||||||
Author(name="Donald E. Knuth"),
|
|
||||||
Author(name="J.K. Rowling"),
|
|
||||||
Author(name="J.R.R. Tolkien"),
|
|
||||||
Author(name="George R.R. Martin"),
|
|
||||||
Author(name="Stephen King"),
|
|
||||||
Author(name="Agatha Christie"),
|
|
||||||
]
|
|
||||||
|
|
||||||
book1 = BookcaseItem(
|
|
||||||
name="The Art of Computer Programming",
|
|
||||||
isbn="9780201896831",
|
|
||||||
)
|
|
||||||
book1.authors.add(authors[0])
|
|
||||||
book1.media_type = media_types[0]
|
|
||||||
book1.shelf = shelfs[59]
|
|
||||||
|
|
||||||
book2 = BookcaseItem(
|
|
||||||
name="Harry Potter and the Philosopher's Stone",
|
|
||||||
isbn="9780747532743",
|
|
||||||
)
|
|
||||||
book2.authors.add(authors[1])
|
|
||||||
book2.media_type = media_types[0]
|
|
||||||
book2.shelf = shelfs[-1]
|
|
||||||
|
|
||||||
book_owned_by_other_user = BookcaseItem(
|
|
||||||
name="Book owned by other user",
|
|
||||||
isbn="9780747532744",
|
|
||||||
)
|
|
||||||
|
|
||||||
book_owned_by_other_user.owner = "other_user"
|
|
||||||
book_owned_by_other_user.authors.add(authors[4])
|
|
||||||
book_owned_by_other_user.media_type = media_types[0]
|
|
||||||
book_owned_by_other_user.shelf = shelfs[-2]
|
|
||||||
|
|
||||||
borrowed_book_more_available = BookcaseItem(
|
|
||||||
name="Borrowed book with more available",
|
|
||||||
isbn="9780747532745",
|
|
||||||
)
|
|
||||||
borrowed_book_more_available.authors.add(authors[5])
|
|
||||||
borrowed_book_more_available.media_type = media_types[0]
|
|
||||||
borrowed_book_more_available.shelf = shelfs[-3]
|
|
||||||
borrowed_book_more_available.amount = 2
|
|
||||||
|
|
||||||
borrowed_book_no_more_available = BookcaseItem(
|
|
||||||
name="Borrowed book with no more available",
|
|
||||||
isbn="9780747532746",
|
|
||||||
)
|
|
||||||
borrowed_book_no_more_available.authors.add(authors[5])
|
|
||||||
borrowed_book_no_more_available.media_type = media_types[0]
|
|
||||||
borrowed_book_no_more_available.shelf = shelfs[-3]
|
|
||||||
|
|
||||||
borrowed_book_people_in_queue = BookcaseItem(
|
|
||||||
name="Borrowed book with people in queue",
|
|
||||||
isbn="9780747532747",
|
|
||||||
)
|
|
||||||
borrowed_book_people_in_queue.authors.add(authors[5])
|
|
||||||
borrowed_book_people_in_queue.media_type = media_types[0]
|
|
||||||
borrowed_book_people_in_queue.shelf = shelfs[-3]
|
|
||||||
|
|
||||||
borrowed_book_by_slabbedask = BookcaseItem(
|
|
||||||
name="Borrowed book by slabbedask",
|
|
||||||
isbn="9780747532748",
|
|
||||||
)
|
|
||||||
borrowed_book_by_slabbedask.authors.add(authors[5])
|
|
||||||
borrowed_book_by_slabbedask.media_type = media_types[0]
|
|
||||||
borrowed_book_by_slabbedask.shelf = shelfs[-3]
|
|
||||||
|
|
||||||
books = [
|
|
||||||
book1,
|
|
||||||
book2,
|
|
||||||
book_owned_by_other_user,
|
|
||||||
borrowed_book_more_available,
|
|
||||||
borrowed_book_no_more_available,
|
|
||||||
borrowed_book_people_in_queue,
|
|
||||||
]
|
|
||||||
|
|
||||||
slabbedask_borrowing = BookcaseItemBorrowing(
|
|
||||||
username="slabbedask",
|
|
||||||
item=borrowed_book_more_available,
|
|
||||||
)
|
|
||||||
slabbedask_borrowing.end_time = datetime.now() - timedelta(days=1)
|
|
||||||
|
|
||||||
borrowings = [
|
|
||||||
BookcaseItemBorrowing(username="user", item=borrowed_book_more_available),
|
|
||||||
BookcaseItemBorrowing(username="user", item=borrowed_book_no_more_available),
|
|
||||||
BookcaseItemBorrowing(username="user", item=borrowed_book_people_in_queue),
|
|
||||||
slabbedask_borrowing,
|
|
||||||
]
|
|
||||||
|
|
||||||
queue = [
|
|
||||||
BookcaseItemBorrowingQueue(username="user", item=borrowed_book_people_in_queue),
|
|
||||||
]
|
|
||||||
|
|
||||||
with open(Path(__file__).parent.parent.parent / "data" / "iso639_1.csv") as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
languages = [Language(name, code) for (code, name) in reader]
|
|
||||||
|
|
||||||
sql_session.add_all(media_types)
|
|
||||||
sql_session.add_all(bookcases)
|
|
||||||
sql_session.add_all(shelfs)
|
|
||||||
sql_session.add_all(languages)
|
|
||||||
sql_session.add_all(authors)
|
|
||||||
sql_session.add_all(books)
|
|
||||||
sql_session.add_all(borrowings)
|
|
||||||
sql_session.add_all(queue)
|
|
||||||
sql_session.commit()
|
|
||||||
print("Added test media types, bookcases and shelfs.")
|
|
||||||
582
uv.lock
generated
582
uv.lock
generated
@@ -1,582 +0,0 @@
|
|||||||
version = 1
|
|
||||||
revision = 2
|
|
||||||
requires-python = ">=3.12"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "alembic"
|
|
||||||
version = "1.17.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "mako" },
|
|
||||||
{ name = "sqlalchemy" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "beautifulsoup4"
|
|
||||||
version = "4.14.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "soupsieve" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "blinker"
|
|
||||||
version = "1.9.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "certifi"
|
|
||||||
version = "2025.11.12"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "charset-normalizer"
|
|
||||||
version = "3.4.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "click"
|
|
||||||
version = "8.3.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flask"
|
|
||||||
version = "3.1.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "blinker" },
|
|
||||||
{ name = "click" },
|
|
||||||
{ name = "itsdangerous" },
|
|
||||||
{ name = "jinja2" },
|
|
||||||
{ name = "markupsafe" },
|
|
||||||
{ name = "werkzeug" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flask-admin"
|
|
||||||
version = "2.0.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "flask" },
|
|
||||||
{ name = "jinja2" },
|
|
||||||
{ name = "markupsafe" },
|
|
||||||
{ name = "werkzeug" },
|
|
||||||
{ name = "wtforms" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/b7/78a1534b4fe1a40bccf79e8e274e1ace6ee1678c804e4c178b1c96e6c8d6/flask_admin-2.0.2.tar.gz", hash = "sha256:1d06aec7efee957972b43f6b08a0bd08d5f4cf9a337d4ece2f17c98abc2a214e", size = 5528977, upload-time = "2025-11-11T21:59:14.782Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/25/a379b8dc388630f1d173544c8b3ebbdb2ad5b60992e531b724d7f8a7ea03/flask_admin-2.0.2-py3-none-any.whl", hash = "sha256:4b3c44068de0fe4630dfcd190cc11231cbbdd7bac315c74c55d1764087b8b273", size = 6459099, upload-time = "2025-11-11T21:59:12.82Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "flask-sqlalchemy"
|
|
||||||
version = "3.1.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "flask" },
|
|
||||||
{ name = "sqlalchemy" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312", size = 81899, upload-time = "2023-09-11T21:42:36.147Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125, upload-time = "2023-09-11T21:42:34.514Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "greenlet"
|
|
||||||
version = "3.3.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna"
|
|
||||||
version = "3.11"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "isbnlib"
|
|
||||||
version = "3.10.14"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/6d/55b9ee89fdfb3aacb92b975a60357c7aa547db358817e16be3b6f8f5d781/isbnlib-3.10.14.tar.gz", hash = "sha256:96f90864c77b01f55fa11e5bfca9fd909501d9842f3bc710d4eab85195d90539", size = 48046, upload-time = "2023-04-26T08:53:05.486Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/ce/1eb1e2afaca49add3185e202ce9c2c6d5779b7eecb20973e43ad804eb2a4/isbnlib-3.10.14-py2.py3-none-any.whl", hash = "sha256:f885b350fc8e600a919ed46e3b07253062cd604af69885455a25a299217b3fe2", size = 52535, upload-time = "2023-04-26T08:53:03.024Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itsdangerous"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jinja2"
|
|
||||||
version = "3.1.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "markupsafe" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libdib"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { git = "https://git.pvv.ntnu.no/Projects/libdib.git#bdc8e0761650e37d754df6b9e0dfbb801951ca53" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "sqlalchemy" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mako"
|
|
||||||
version = "1.3.10"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "markupsafe" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markupsafe"
|
|
||||||
version = "3.0.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pastel"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "poethepoet"
|
|
||||||
version = "0.38.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "pastel" },
|
|
||||||
{ name = "pyyaml" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/14/d1f795f314c4bf3ad6d64216e370bdfda73093ed76e979485778b655a7ac/poethepoet-0.38.0.tar.gz", hash = "sha256:aeeb2f0a2cf0d3afa833976eff3ac7b8f5e472ae64171824900d79d3c68163c7", size = 77339, upload-time = "2025-11-23T13:51:28.246Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/89/2bf7d43ef4b0d60f446933ae9d3649f95c2c45c47b6736d121b602c28361/poethepoet-0.38.0-py3-none-any.whl", hash = "sha256:214bd9fcb348ff3dfd1466579d67e0c02242451a7044aced1a79641adef9cad0", size = 101938, upload-time = "2025-11-23T13:51:26.518Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "psycopg2-binary"
|
|
||||||
version = "2.9.11"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pyyaml"
|
|
||||||
version = "6.0.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "requests"
|
|
||||||
version = "2.32.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "charset-normalizer" },
|
|
||||||
{ name = "idna" },
|
|
||||||
{ name = "urllib3" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "soupsieve"
|
|
||||||
version = "2.8"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sqlalchemy"
|
|
||||||
version = "2.0.44"
|
|
||||||
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/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/c4/59c7c9b068e6813c898b771204aad36683c96318ed12d4233e1b18762164/sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", size = 2139675, upload-time = "2025-10-10T16:03:31.064Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/ae/eeb0920537a6f9c5a3708e4a5fc55af25900216bdb4847ec29cfddf3bf3a/sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", size = 2127726, upload-time = "2025-10-10T16:03:35.934Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/d5/2ebbabe0379418eda8041c06b0b551f213576bfe4c2f09d77c06c07c8cc5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", size = 3327603, upload-time = "2025-10-10T15:35:28.322Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/e5/5aa65852dadc24b7d8ae75b7efb8d19303ed6ac93482e60c44a585930ea5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", size = 3337842, upload-time = "2025-10-10T15:43:45.431Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/92/648f1afd3f20b71e880ca797a960f638d39d243e233a7082c93093c22378/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", size = 3264558, upload-time = "2025-10-10T15:35:29.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/cf/e27d7ee61a10f74b17740918e23cbc5bc62011b48282170dc4c66da8ec0f/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", size = 3301570, upload-time = "2025-10-10T15:43:48.407Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/3d/3116a9a7b63e780fb402799b6da227435be878b6846b192f076d2f838654/sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", size = 2103447, upload-time = "2025-10-10T15:03:21.678Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/83/24690e9dfc241e6ab062df82cc0df7f4231c79ba98b273fa496fb3dd78ed/sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", size = 2130912, upload-time = "2025-10-10T15:03:24.656Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[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" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "urllib3"
|
|
||||||
version = "2.6.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/43/554c2569b62f49350597348fc3ac70f786e3c32e7f19d266e19817812dd3/urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1", size = 432585, upload-time = "2025-12-05T15:08:47.885Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/1a/9ffe814d317c5224166b23e7c47f606d6e473712a2fad0f704ea9b99f246/urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", size = 131083, upload-time = "2025-12-05T15:08:45.983Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "werkzeug"
|
|
||||||
version = "3.1.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "markupsafe" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "worblehat"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { editable = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "alembic" },
|
|
||||||
{ name = "beautifulsoup4" },
|
|
||||||
{ name = "click" },
|
|
||||||
{ name = "flask" },
|
|
||||||
{ name = "flask-admin" },
|
|
||||||
{ name = "flask-sqlalchemy" },
|
|
||||||
{ name = "isbnlib" },
|
|
||||||
{ name = "libdib" },
|
|
||||||
{ name = "psycopg2-binary" },
|
|
||||||
{ name = "requests" },
|
|
||||||
{ name = "sqlalchemy" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "poethepoet" },
|
|
||||||
{ name = "werkzeug" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "alembic", specifier = ">=1.17" },
|
|
||||||
{ name = "beautifulsoup4", specifier = ">=4.14" },
|
|
||||||
{ name = "click", specifier = ">=8.3" },
|
|
||||||
{ name = "flask", specifier = ">=3.0" },
|
|
||||||
{ name = "flask-admin", specifier = ">=2.0" },
|
|
||||||
{ name = "flask-sqlalchemy", specifier = ">=3.1" },
|
|
||||||
{ name = "isbnlib", specifier = ">=3.10" },
|
|
||||||
{ name = "libdib", git = "https://git.pvv.ntnu.no/Projects/libdib.git" },
|
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.9" },
|
|
||||||
{ name = "requests", specifier = ">=2.32" },
|
|
||||||
{ name = "sqlalchemy", specifier = ">=2.0" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
|
||||||
dev = [
|
|
||||||
{ name = "poethepoet" },
|
|
||||||
{ name = "werkzeug" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wtforms"
|
|
||||||
version = "3.2.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "markupsafe" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/01/e4/633d080897e769ed5712dcfad626e55dbd6cf45db0ff4d9884315c6a82da/wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682", size = 137801, upload-time = "2024-10-21T11:34:00.108Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454, upload-time = "2024-10-21T11:33:58.44Z" },
|
|
||||||
]
|
|
||||||
1
worblehat/cli/__init__.py
Normal file
1
worblehat/cli/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .main import WorblehatCli
|
||||||
234
worblehat/cli/main.py
Normal file
234
worblehat/cli/main.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
event,
|
||||||
|
select,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.services import (
|
||||||
|
create_bookcase_item_from_isbn,
|
||||||
|
is_valid_isbn,
|
||||||
|
)
|
||||||
|
|
||||||
|
from worblehat.models import *
|
||||||
|
|
||||||
|
from .prompt_utils import *
|
||||||
|
from .subclis import (
|
||||||
|
AdvancedOptionsCli,
|
||||||
|
BookcaseItemCli,
|
||||||
|
select_bookcase_shelf,
|
||||||
|
SearchCli,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Category seems to have been forgotten. Maybe relevant interactivity should be added?
|
||||||
|
# However, is there anyone who are going to search by category rather than just look in
|
||||||
|
# the shelves?
|
||||||
|
|
||||||
|
class WorblehatCli(NumberedCmd):
|
||||||
|
def __init__(self, sql_session: Session):
|
||||||
|
super().__init__()
|
||||||
|
self.sql_session = sql_session
|
||||||
|
self.sql_session_dirty = False
|
||||||
|
|
||||||
|
@event.listens_for(self.sql_session, 'after_flush')
|
||||||
|
def mark_session_as_dirty(*_):
|
||||||
|
self.sql_session_dirty = True
|
||||||
|
self.prompt_header = f'(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
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run_with_safe_exit_wrapper(cls, sql_session: Session):
|
||||||
|
tool = cls(sql_session)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
tool.cmdloop()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
if not tool.sql_session_dirty:
|
||||||
|
exit(0)
|
||||||
|
try:
|
||||||
|
print()
|
||||||
|
if prompt_yes_no('Are you sure you want to exit without saving?', default=False):
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
if tool.sql_session is not None:
|
||||||
|
tool.sql_session.rollback()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def do_list_bookcases(self, _: str):
|
||||||
|
bookcase_shelfs = self.sql_session.scalars(
|
||||||
|
select(BookcaseShelf)
|
||||||
|
.join(Bookcase)
|
||||||
|
.order_by(
|
||||||
|
Bookcase.name,
|
||||||
|
BookcaseShelf.column,
|
||||||
|
BookcaseShelf.row,
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
bookcase_uid = None
|
||||||
|
for shelf in bookcase_shelfs:
|
||||||
|
if shelf.bookcase.uid != bookcase_uid:
|
||||||
|
print(shelf.bookcase.short_str())
|
||||||
|
bookcase_uid = shelf.bookcase.uid
|
||||||
|
|
||||||
|
print(f' {shelf.short_str()} - {sum(i.amount for i in shelf.items)} items')
|
||||||
|
|
||||||
|
|
||||||
|
def do_show_bookcase(self, arg: str):
|
||||||
|
bookcase_selector = InteractiveItemSelector(
|
||||||
|
cls = Bookcase,
|
||||||
|
sql_session = self.sql_session,
|
||||||
|
)
|
||||||
|
bookcase_selector.cmdloop()
|
||||||
|
bookcase = bookcase_selector.result
|
||||||
|
|
||||||
|
for shelf in bookcase.shelfs:
|
||||||
|
print(shelf.short_str())
|
||||||
|
for item in shelf.items:
|
||||||
|
print(f' {item.name} - {item.amount} copies')
|
||||||
|
|
||||||
|
|
||||||
|
def _create_bookcase_item(self, isbn: str):
|
||||||
|
bookcase_item = create_bookcase_item_from_isbn(isbn, self.sql_session)
|
||||||
|
if bookcase_item is None:
|
||||||
|
print(f'Could not find data about item with ISBN {isbn} online.')
|
||||||
|
print(f'If you think this is not due to a bug, please add the book to openlibrary.org before continuing.')
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(dedent(f"""
|
||||||
|
Found item:
|
||||||
|
title: {bookcase_item.name}
|
||||||
|
authors: {', '.join(a.name for a in bookcase_item.authors)}
|
||||||
|
language: {bookcase_item.language}
|
||||||
|
"""))
|
||||||
|
|
||||||
|
print('Please select the bookcase where the item is placed:')
|
||||||
|
bookcase_selector = InteractiveItemSelector(
|
||||||
|
cls = Bookcase,
|
||||||
|
sql_session = self.sql_session,
|
||||||
|
)
|
||||||
|
bookcase_selector.cmdloop()
|
||||||
|
bookcase = bookcase_selector.result
|
||||||
|
|
||||||
|
bookcase_item.shelf = select_bookcase_shelf(bookcase, self.sql_session)
|
||||||
|
|
||||||
|
print('Please select the items media type:')
|
||||||
|
media_type_selector = InteractiveItemSelector(
|
||||||
|
cls = MediaType,
|
||||||
|
sql_session = self.sql_session,
|
||||||
|
default = self.sql_session.scalars(
|
||||||
|
select(MediaType)
|
||||||
|
.where(MediaType.name.ilike("book")),
|
||||||
|
).one(),
|
||||||
|
)
|
||||||
|
|
||||||
|
media_type_selector.cmdloop()
|
||||||
|
bookcase_item.media_type = media_type_selector.result
|
||||||
|
|
||||||
|
username = input('Who owns this book? [PVV]> ')
|
||||||
|
if username != '':
|
||||||
|
bookcase_item.owner = username
|
||||||
|
|
||||||
|
self.sql_session.add(bookcase_item)
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def default(self, isbn: str):
|
||||||
|
isbn = isbn.strip()
|
||||||
|
if not is_valid_isbn(isbn):
|
||||||
|
super()._default(isbn)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (existing_item := self.sql_session.scalars(
|
||||||
|
select(BookcaseItem)
|
||||||
|
.where(BookcaseItem.isbn == isbn)
|
||||||
|
).one_or_none()) is not None:
|
||||||
|
print(f'\nFound existing item for isbn "{isbn}"')
|
||||||
|
BookcaseItemCli(
|
||||||
|
sql_session = self.sql_session,
|
||||||
|
bookcase_item = existing_item,
|
||||||
|
).cmdloop()
|
||||||
|
return
|
||||||
|
|
||||||
|
if prompt_yes_no(f"Could not find item with ISBN '{isbn}'.\nWould you like to create it?", default=True):
|
||||||
|
self._create_bookcase_item(isbn)
|
||||||
|
|
||||||
|
|
||||||
|
def do_search(self, _: str):
|
||||||
|
search_cli = SearchCli(self.sql_session)
|
||||||
|
search_cli.cmdloop()
|
||||||
|
if search_cli.result is not None:
|
||||||
|
BookcaseItemCli(
|
||||||
|
sql_session = self.sql_session,
|
||||||
|
bookcase_item = search_cli.result,
|
||||||
|
).cmdloop()
|
||||||
|
|
||||||
|
|
||||||
|
def do_advanced(self, _: str):
|
||||||
|
AdvancedOptionsCli(self.sql_session).cmdloop()
|
||||||
|
|
||||||
|
|
||||||
|
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': 'Choose / Add item with its ISBN',
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
'f': do_list_bookcases,
|
||||||
|
'doc': 'List all bookcases',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
'f': do_search,
|
||||||
|
'doc': 'Search',
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
'f': do_show_bookcase,
|
||||||
|
'doc': 'Show a bookcase, and its items',
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
'f': do_save,
|
||||||
|
'doc': 'Save changes',
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
'f': do_abort,
|
||||||
|
'doc': 'Abort changes',
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
'f': do_advanced,
|
||||||
|
'doc': 'Advanced options',
|
||||||
|
},
|
||||||
|
9: {
|
||||||
|
'f': do_exit,
|
||||||
|
'doc': 'Exit',
|
||||||
|
},
|
||||||
|
}
|
||||||
211
worblehat/cli/prompt_utils.py
Normal file
211
worblehat/cli/prompt_utils.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
from cmd import Cmd
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
def prompt_yes_no(question: str, default: bool | None = None) -> bool:
|
||||||
|
prompt = {
|
||||||
|
None: '[y/n]',
|
||||||
|
True: '[Y/n]',
|
||||||
|
False: '[y/N]',
|
||||||
|
}[default]
|
||||||
|
|
||||||
|
while not any([
|
||||||
|
(answer := input(f'{question} {prompt} ').lower()) in ('y','n'),
|
||||||
|
(default != None and answer.strip() == '')
|
||||||
|
]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'y': True,
|
||||||
|
'n': False,
|
||||||
|
'': default,
|
||||||
|
}[answer]
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(date: datetime):
|
||||||
|
return date.strftime("%a %b %d, %Y")
|
||||||
|
|
||||||
|
|
||||||
|
class InteractiveItemSelector(Cmd):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cls: type,
|
||||||
|
sql_session: Session,
|
||||||
|
execute_selection: Callable[[Session, type, str], list[Any]] = lambda session, cls, arg: session.scalars(
|
||||||
|
select(cls)
|
||||||
|
.where(cls.name == arg),
|
||||||
|
).all(),
|
||||||
|
complete_selection: Callable[[Session, type, str], list[str]] = lambda session, cls, text: session.scalars(
|
||||||
|
select(cls.name)
|
||||||
|
.where(cls.name.ilike(f'{text}%')),
|
||||||
|
).all(),
|
||||||
|
default: Any | None = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This is a utility class for prompting the user to select an
|
||||||
|
item from the database. The default functions assumes that
|
||||||
|
the item has a name attribute, and that the name is unique.
|
||||||
|
However, this can be overridden by passing in custom functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.cls = cls
|
||||||
|
self.sql_session = sql_session
|
||||||
|
self.execute_selection = execute_selection
|
||||||
|
self.complete_selection = complete_selection
|
||||||
|
self.default_item = default
|
||||||
|
|
||||||
|
if default is not None:
|
||||||
|
self.prompt = f'Select {cls.__name__} [{default.name}]> '
|
||||||
|
else:
|
||||||
|
self.prompt = f'Select {cls.__name__}> '
|
||||||
|
|
||||||
|
|
||||||
|
def emptyline(self) -> bool:
|
||||||
|
if self.default_item is not None:
|
||||||
|
self.result = self.default_item
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def default(self, arg: str):
|
||||||
|
result = self.execute_selection(self.sql_session, self.cls, arg)
|
||||||
|
|
||||||
|
if len(result) != 1:
|
||||||
|
print(f'No such {self.cls.__name__} found: {arg}')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.result = result[0]
|
||||||
|
return True
|
||||||
|
|
||||||
|
# TODO: Override this function to not act as an argument completer
|
||||||
|
# but to complete the entire value name
|
||||||
|
def completedefault(self, text: str, line: str, *_) -> list[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def completenames(self, text: str, *_) -> list[str]:
|
||||||
|
x = self.complete_selection(self.sql_session, self.cls, text)
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
class NumberedCmd(Cmd):
|
||||||
|
"""
|
||||||
|
This is a utility class for creating a numbered command line.
|
||||||
|
|
||||||
|
It will automatically generate a prompt that lists all the
|
||||||
|
available commands, and will automatically call the correct
|
||||||
|
function based on the user input.
|
||||||
|
|
||||||
|
If the user input is not a number, it will call the default
|
||||||
|
function, which can be overridden by the subclass.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
class MyCmd(NumberedCmd):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def do_foo(self, arg: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_bar(self, arg: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
funcs = {
|
||||||
|
1: {
|
||||||
|
'f': do_foo,
|
||||||
|
'doc': 'do foo',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
'f': do_bar,
|
||||||
|
'doc': 'do bar',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
prompt_header: str | None = None
|
||||||
|
funcs: dict[int, dict[str, str | Callable[[Any, str], bool | None]]]
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_usage_list(self) -> str:
|
||||||
|
result = ''
|
||||||
|
for i, func in self.funcs.items():
|
||||||
|
if i == 0:
|
||||||
|
i = '*'
|
||||||
|
result += f'{i}) {func["doc"]}\n'
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _default(self, arg: str):
|
||||||
|
try:
|
||||||
|
i = int(arg)
|
||||||
|
self.funcs[i]
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
return
|
||||||
|
|
||||||
|
return self.funcs[i]['f'](self, arg)
|
||||||
|
|
||||||
|
|
||||||
|
def default(self, arg: str):
|
||||||
|
return self._default(arg)
|
||||||
|
|
||||||
|
|
||||||
|
def _postcmd(self, stop: bool, _: str) -> bool:
|
||||||
|
if not stop:
|
||||||
|
print()
|
||||||
|
print('-----------------')
|
||||||
|
print()
|
||||||
|
return stop
|
||||||
|
|
||||||
|
|
||||||
|
def postcmd(self, stop: bool, line: str) -> bool:
|
||||||
|
return self._postcmd(stop, line)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prompt(self):
|
||||||
|
result = ''
|
||||||
|
|
||||||
|
if self.prompt_header != None:
|
||||||
|
result += self.prompt_header + '\n'
|
||||||
|
|
||||||
|
result += self._generate_usage_list()
|
||||||
|
|
||||||
|
if self.lastcmd == '':
|
||||||
|
result += f'> '
|
||||||
|
else:
|
||||||
|
result += f'[{self.lastcmd}]> '
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class NumberedItemSelector(NumberedCmd):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
items: list[Any],
|
||||||
|
stringify: Callable[[Any], str] = lambda x: str(x),
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.items = items
|
||||||
|
self.stringify = stringify
|
||||||
|
self.funcs = {
|
||||||
|
i: {
|
||||||
|
'f': self._select_item,
|
||||||
|
'doc': self.stringify(item),
|
||||||
|
}
|
||||||
|
for i, item in enumerate(items, start=1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _select_item(self, *a):
|
||||||
|
self.result = self.items[int(self.lastcmd)-1]
|
||||||
|
return True
|
||||||
@@ -2,10 +2,3 @@ from .advanced_options import AdvancedOptionsCli
|
|||||||
from .bookcase_item import BookcaseItemCli
|
from .bookcase_item import BookcaseItemCli
|
||||||
from .bookcase_shelf_selector import select_bookcase_shelf
|
from .bookcase_shelf_selector import select_bookcase_shelf
|
||||||
from .search import SearchCli
|
from .search import SearchCli
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"AdvancedOptionsCli",
|
|
||||||
"BookcaseItemCli",
|
|
||||||
"select_bookcase_shelf",
|
|
||||||
"SearchCli",
|
|
||||||
]
|
|
||||||
111
worblehat/cli/subclis/advanced_options.py
Normal file
111
worblehat/cli/subclis/advanced_options.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.cli.prompt_utils import (
|
||||||
|
InteractiveItemSelector,
|
||||||
|
NumberedCmd,
|
||||||
|
format_date,
|
||||||
|
prompt_yes_no,
|
||||||
|
)
|
||||||
|
from worblehat.models import Bookcase, BookcaseShelf
|
||||||
|
|
||||||
|
class AdvancedOptionsCli(NumberedCmd):
|
||||||
|
def __init__(self, sql_session: Session):
|
||||||
|
super().__init__()
|
||||||
|
self.sql_session = sql_session
|
||||||
|
|
||||||
|
|
||||||
|
def do_add_bookcase(self, _: str):
|
||||||
|
while True:
|
||||||
|
name = input('Name of bookcase> ')
|
||||||
|
if name == '':
|
||||||
|
print('Error: name cannot be empty')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.sql_session.scalars(
|
||||||
|
select(Bookcase)
|
||||||
|
.where(Bookcase.name == name)
|
||||||
|
).one_or_none() is not None:
|
||||||
|
print(f'Error: a bookcase with name {name} already exists')
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
description = input('Description of bookcase> ')
|
||||||
|
if description == '':
|
||||||
|
description = None
|
||||||
|
|
||||||
|
bookcase = Bookcase(name, description)
|
||||||
|
self.sql_session.add(bookcase)
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def do_add_bookcase_shelf(self, arg: str):
|
||||||
|
bookcase_selector = InteractiveItemSelector(
|
||||||
|
cls = Bookcase,
|
||||||
|
sql_session = self.sql_session,
|
||||||
|
)
|
||||||
|
bookcase_selector.cmdloop()
|
||||||
|
bookcase = bookcase_selector.result
|
||||||
|
|
||||||
|
while True:
|
||||||
|
column = input('Column> ')
|
||||||
|
try:
|
||||||
|
column = int(column)
|
||||||
|
except ValueError:
|
||||||
|
print('Error: column must be a number')
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
while True:
|
||||||
|
row = input('Row> ')
|
||||||
|
try:
|
||||||
|
row = int(row)
|
||||||
|
except ValueError:
|
||||||
|
print('Error: row must be a number')
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.sql_session.scalars(
|
||||||
|
select(BookcaseShelf)
|
||||||
|
.where(
|
||||||
|
BookcaseShelf.bookcase == bookcase,
|
||||||
|
BookcaseShelf.column == column,
|
||||||
|
BookcaseShelf.row == row,
|
||||||
|
)
|
||||||
|
).one_or_none() is not None:
|
||||||
|
print(f'Error: a bookshelf in bookcase {bookcase.name} with position c{column}-r{row} already exists')
|
||||||
|
return
|
||||||
|
|
||||||
|
description = input('Description> ')
|
||||||
|
if description == '':
|
||||||
|
description = None
|
||||||
|
|
||||||
|
shelf = BookcaseShelf(
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
bookcase,
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
self.sql_session.add(shelf)
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def do_done(self, _: str):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
funcs = {
|
||||||
|
1: {
|
||||||
|
'f': do_add_bookcase,
|
||||||
|
'doc': 'Add bookcase',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
'f': do_add_bookcase_shelf,
|
||||||
|
'doc': 'Add bookcase shelf',
|
||||||
|
},
|
||||||
|
9: {
|
||||||
|
'f': do_done,
|
||||||
|
'doc': 'Done',
|
||||||
|
},
|
||||||
|
}
|
||||||
346
worblehat/cli/subclis/bookcase_item.py
Normal file
346
worblehat/cli/subclis/bookcase_item.py
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from textwrap import dedent
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.cli.prompt_utils import (
|
||||||
|
InteractiveItemSelector,
|
||||||
|
NumberedCmd,
|
||||||
|
format_date,
|
||||||
|
prompt_yes_no,
|
||||||
|
)
|
||||||
|
from worblehat.models import (
|
||||||
|
Bookcase,
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseItemBorrowingQueue,
|
||||||
|
Language,
|
||||||
|
MediaType,
|
||||||
|
)
|
||||||
|
from worblehat.services.bookcase_item import (
|
||||||
|
create_bookcase_item_from_isbn,
|
||||||
|
is_valid_isbn,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .bookcase_shelf_selector import select_bookcase_shelf
|
||||||
|
|
||||||
|
def _selected_bookcase_item_prompt(bookcase_item: BookcaseItem) -> str:
|
||||||
|
amount_borrowed = len(bookcase_item.borrowings)
|
||||||
|
return dedent(f'''
|
||||||
|
Item: {bookcase_item.name}
|
||||||
|
ISBN: {bookcase_item.isbn}
|
||||||
|
Authors: {', '.join(a.name for a in bookcase_item.authors)}
|
||||||
|
Bookcase: {bookcase_item.shelf.bookcase.short_str()}
|
||||||
|
Shelf: {bookcase_item.shelf.short_str()}
|
||||||
|
Amount: {bookcase_item.amount - amount_borrowed}/{bookcase_item.amount}
|
||||||
|
''')
|
||||||
|
|
||||||
|
class BookcaseItemCli(NumberedCmd):
|
||||||
|
def __init__(self, sql_session: Session, bookcase_item: BookcaseItem):
|
||||||
|
super().__init__()
|
||||||
|
self.sql_session = sql_session
|
||||||
|
self.bookcase_item = bookcase_item
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prompt_header(self) -> str:
|
||||||
|
return _selected_bookcase_item_prompt(self.bookcase_item)
|
||||||
|
|
||||||
|
|
||||||
|
def do_update_data(self, _: str):
|
||||||
|
item = create_bookcase_item_from_isbn(self.sql_session, self.bookcase_item.isbn)
|
||||||
|
self.bookcase_item.name = item.name
|
||||||
|
# TODO: Remove any old authors
|
||||||
|
self.bookcase_item.authors = item.authors
|
||||||
|
self.bookcase_item.language = item.language
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def do_edit(self, arg: str):
|
||||||
|
EditBookcaseCli(self.sql_session, self.bookcase_item, self).cmdloop()
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _prompt_username() -> str:
|
||||||
|
while True:
|
||||||
|
username = input('Username: ')
|
||||||
|
if prompt_yes_no(f'Is {username} correct?', default = True):
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
def _has_active_borrowing(self, username: str) -> bool:
|
||||||
|
return self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowing.username == username,
|
||||||
|
BookcaseItemBorrowing.item == self.bookcase_item,
|
||||||
|
BookcaseItemBorrowing.delivered.is_(None),
|
||||||
|
)
|
||||||
|
).one_or_none() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_borrowing_queue_item(self, username: str) -> bool:
|
||||||
|
return self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowingQueue.username == username,
|
||||||
|
BookcaseItemBorrowingQueue.item == self.bookcase_item,
|
||||||
|
)
|
||||||
|
).one_or_none() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def do_borrow(self, _: str):
|
||||||
|
active_borrowings = self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowing.item == self.bookcase_item,
|
||||||
|
BookcaseItemBorrowing.delivered.is_(None),
|
||||||
|
)
|
||||||
|
.order_by(BookcaseItemBorrowing.end_time)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if len(active_borrowings) >= self.bookcase_item.amount:
|
||||||
|
print('This item is currently not available')
|
||||||
|
print()
|
||||||
|
print('Active borrowings:')
|
||||||
|
|
||||||
|
for b in active_borrowings:
|
||||||
|
print(f' {b.username} - Until {format_date(b.end_time)}')
|
||||||
|
|
||||||
|
if len(self.bookcase_item.borrowing_queue) > 0:
|
||||||
|
print('Borrowing queue:')
|
||||||
|
for i, b in enumerate(self.bookcase_item.borrowing_queue):
|
||||||
|
print(f' {i + 1} - {b.username}')
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not prompt_yes_no('Would you like to enter the borrowing queue?', default = True):
|
||||||
|
return
|
||||||
|
username = self._prompt_username()
|
||||||
|
|
||||||
|
if self._has_active_borrowing(username):
|
||||||
|
print('You already have an active borrowing')
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._has_borrowing_queue_item(username):
|
||||||
|
print('You are already in the borrowing queue')
|
||||||
|
return
|
||||||
|
|
||||||
|
borrowing_queue_item = BookcaseItemBorrowingQueue(username, self.bookcase_item)
|
||||||
|
self.sql_session.add(borrowing_queue_item)
|
||||||
|
print(f'{username} entered the queue!')
|
||||||
|
return
|
||||||
|
|
||||||
|
username = self._prompt_username()
|
||||||
|
|
||||||
|
borrowing_item = BookcaseItemBorrowing(username, self.bookcase_item)
|
||||||
|
self.sql_session.add(borrowing_item)
|
||||||
|
self.sql_session.flush()
|
||||||
|
print(f'Successfully borrowed the item. Please deliver it back by {format_date(borrowing_item.end_time)}')
|
||||||
|
|
||||||
|
def do_deliver(self, _: str):
|
||||||
|
borrowings = self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing)
|
||||||
|
.join(BookcaseItem, BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid)
|
||||||
|
.where(BookcaseItem.isbn == self.bookcase_item.isbn)
|
||||||
|
.order_by(BookcaseItemBorrowing.username)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if len(borrowings) == 0:
|
||||||
|
print('No one seems to have borrowed this item')
|
||||||
|
return
|
||||||
|
|
||||||
|
print('Borrowers:')
|
||||||
|
for i, b in enumerate(borrowings):
|
||||||
|
print(f' {i + 1}) {b.username}')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
selection = int(input('> '))
|
||||||
|
except ValueError:
|
||||||
|
print('Error: selection must be an integer')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if selection < 1 or selection > len(borrowings):
|
||||||
|
print('Error: selection out of range')
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
borrowing = borrowings[selection - 1]
|
||||||
|
borrowing.delivered = datetime.now()
|
||||||
|
self.sql_session.flush()
|
||||||
|
print(f'Successfully delivered the item for {borrowing.username}')
|
||||||
|
|
||||||
|
|
||||||
|
def do_done(self, _: str):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
funcs = {
|
||||||
|
1: {
|
||||||
|
'f': do_borrow,
|
||||||
|
'doc': 'Borrow',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
'f': do_deliver,
|
||||||
|
'doc': 'Deliver',
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
'f': do_edit,
|
||||||
|
'doc': 'Edit',
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
'f': do_update_data,
|
||||||
|
'doc': 'Pull updated data from online databases',
|
||||||
|
},
|
||||||
|
9: {
|
||||||
|
'f': do_done,
|
||||||
|
'doc': 'Done',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditBookcaseCli(NumberedCmd):
|
||||||
|
def __init__(self, sql_session: Session, bookcase_item: BookcaseItem, parent: BookcaseItemCli):
|
||||||
|
super().__init__()
|
||||||
|
self.sql_session = sql_session
|
||||||
|
self.bookcase_item = bookcase_item
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prompt_header(self) -> str:
|
||||||
|
return _selected_bookcase_item_prompt(self.bookcase_item)
|
||||||
|
|
||||||
|
def do_name(self, _: str):
|
||||||
|
while True:
|
||||||
|
name = input('New name> ')
|
||||||
|
if name == '':
|
||||||
|
print('Error: name cannot be empty')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.sql_session.scalars(
|
||||||
|
select(BookcaseItem)
|
||||||
|
.where(BookcaseItem.name == name)
|
||||||
|
).one_or_none() is not None:
|
||||||
|
print(f'Error: an item with name {name} already exists')
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
self.bookcase_item.name = name
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def do_isbn(self, _: str):
|
||||||
|
while True:
|
||||||
|
isbn = input('New ISBN> ')
|
||||||
|
if isbn == '':
|
||||||
|
print('Error: ISBN cannot be empty')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not is_valid_isbn(isbn):
|
||||||
|
print('Error: ISBN is not valid')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.sql_session.scalars(
|
||||||
|
select(BookcaseItem)
|
||||||
|
.where(BookcaseItem.isbn == isbn)
|
||||||
|
).one_or_none() is not None:
|
||||||
|
print(f'Error: an item with ISBN {isbn} already exists')
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
self.bookcase_item.isbn = isbn
|
||||||
|
|
||||||
|
if prompt_yes_no('Update data from online databases?'):
|
||||||
|
self.parent.do_update_data('')
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def do_language(self, _: str):
|
||||||
|
language_selector = InteractiveItemSelector(
|
||||||
|
Language,
|
||||||
|
self.sql_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.bookcase_item.language = language_selector.result
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def do_media_type(self, _: str):
|
||||||
|
media_type_selector = InteractiveItemSelector(
|
||||||
|
MediaType,
|
||||||
|
self.sql_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.bookcase_item.media_type = media_type_selector.result
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def do_amount(self, _: str):
|
||||||
|
while (new_amount := input(f'New amount [{self.bookcase_item.amount}]> ')) != '':
|
||||||
|
try:
|
||||||
|
new_amount = int(new_amount)
|
||||||
|
except ValueError:
|
||||||
|
print('Error: amount must be an integer')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if new_amount < 1:
|
||||||
|
print('Error: amount must be greater than 0')
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
self.bookcase_item.amount = new_amount
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def do_shelf(self, _: str):
|
||||||
|
bookcase_selector = InteractiveItemSelector(
|
||||||
|
Bookcase,
|
||||||
|
self.sql_session,
|
||||||
|
)
|
||||||
|
bookcase_selector.cmdloop()
|
||||||
|
bookcase = bookcase_selector.result
|
||||||
|
|
||||||
|
shelf = select_bookcase_shelf(bookcase, self.sql_session)
|
||||||
|
|
||||||
|
self.bookcase_item.shelf = shelf
|
||||||
|
self.sql_session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def do_done(self, _: str):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
funcs = {
|
||||||
|
1: {
|
||||||
|
'f': do_name,
|
||||||
|
'doc': 'Change name',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
'f': do_isbn,
|
||||||
|
'doc': 'Change ISBN',
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
'f': do_language,
|
||||||
|
'doc': 'Change language',
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
'f': do_media_type,
|
||||||
|
'doc': 'Change media type',
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
'f': do_amount,
|
||||||
|
'doc': 'Change amount',
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
'f': do_shelf,
|
||||||
|
'doc': 'Change shelf',
|
||||||
|
},
|
||||||
|
9: {
|
||||||
|
'f': do_done,
|
||||||
|
'doc': 'Done',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,24 +1,22 @@
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from libdib.repl import InteractiveItemSelector
|
from worblehat.cli.prompt_utils import InteractiveItemSelector
|
||||||
|
|
||||||
from worblehat.models import (
|
from worblehat.models import (
|
||||||
Bookcase,
|
Bookcase,
|
||||||
BookcaseShelf,
|
BookcaseShelf,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def select_bookcase_shelf(
|
def select_bookcase_shelf(
|
||||||
bookcase: Bookcase,
|
bookcase: Bookcase,
|
||||||
sql_session: Session,
|
sql_session: Session,
|
||||||
prompt: str = "Please select the shelf where the item is placed (col-row):",
|
prompt: str = "Please select the shelf where the item is placed (col-row):"
|
||||||
) -> BookcaseShelf:
|
) -> BookcaseShelf:
|
||||||
def __complete_bookshelf_selection(session: Session, cls: type, arg: str):
|
def __complete_bookshelf_selection(session: Session, cls: type, arg: str):
|
||||||
args = arg.split("-")
|
args = arg.split('-')
|
||||||
query = select(cls.row, cls.column).where(cls.bookcase == bookcase)
|
query = select(cls.row, cls.column).where(cls.bookcase == bookcase)
|
||||||
try:
|
try:
|
||||||
if arg != "" and len(args) > 0:
|
if arg != '' and len(args) > 0:
|
||||||
query = query.where(cls.column == int(args[0]))
|
query = query.where(cls.column == int(args[0]))
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
query = query.where(cls.row == int(args[1]))
|
query = query.where(cls.row == int(args[1]))
|
||||||
@@ -26,20 +24,21 @@ def select_bookcase_shelf(
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
result = session.execute(query).all()
|
result = session.execute(query).all()
|
||||||
return [f"{c}-{r}" for r, c in result]
|
return [f"{c}-{r}" for r,c in result]
|
||||||
|
|
||||||
print(prompt)
|
print(prompt)
|
||||||
bookcase_shelf_selector = InteractiveItemSelector(
|
bookcase_shelf_selector = InteractiveItemSelector(
|
||||||
cls=BookcaseShelf,
|
cls = BookcaseShelf,
|
||||||
sql_session=sql_session,
|
sql_session = sql_session,
|
||||||
execute_selection=lambda session, cls, arg: session.scalars(
|
execute_selection = lambda session, cls, arg: session.scalars(
|
||||||
select(cls).where(
|
select(cls)
|
||||||
|
.where(
|
||||||
cls.bookcase == bookcase,
|
cls.bookcase == bookcase,
|
||||||
cls.column == int(arg.split("-")[0]),
|
cls.column == int(arg.split('-')[0]),
|
||||||
cls.row == int(arg.split("-")[1]),
|
cls.row == int(arg.split('-')[1]),
|
||||||
)
|
)
|
||||||
).all(),
|
).all(),
|
||||||
complete_selection=__complete_bookshelf_selection,
|
complete_selection = __complete_bookshelf_selection,
|
||||||
)
|
)
|
||||||
|
|
||||||
bookcase_shelf_selector.cmdloop()
|
bookcase_shelf_selector.cmdloop()
|
||||||
@@ -2,7 +2,7 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
|
||||||
from libdib.repl import (
|
from worblehat.cli.prompt_utils import (
|
||||||
NumberedCmd,
|
NumberedCmd,
|
||||||
NumberedItemSelector,
|
NumberedItemSelector,
|
||||||
)
|
)
|
||||||
@@ -13,53 +13,55 @@ class SearchCli(NumberedCmd):
|
|||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.sql_session = sql_session
|
self.sql_session = sql_session
|
||||||
self.result = None
|
|
||||||
|
|
||||||
def do_search_all(self, _: str):
|
def do_search_all(self, _: str):
|
||||||
print("TODO: Implement search all")
|
print('TODO: Implement search all')
|
||||||
|
|
||||||
|
|
||||||
def do_search_title(self, _: str):
|
def do_search_title(self, _: str):
|
||||||
while (input_text := input("Enter title: ")) == "":
|
while (input_text := input('Enter title: ')) == '':
|
||||||
pass
|
pass
|
||||||
|
|
||||||
items = self.sql_session.scalars(
|
items = self.sql_session.scalars(
|
||||||
select(BookcaseItem).where(BookcaseItem.name.ilike(f"%{input_text}%")),
|
select(BookcaseItem)
|
||||||
|
.where(BookcaseItem.name.ilike(f'%{input_text}%')),
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
if len(items) == 0:
|
if len(items) == 0:
|
||||||
print("No items found.")
|
print('No items found.')
|
||||||
return
|
return
|
||||||
|
|
||||||
selector = NumberedItemSelector(
|
selector = NumberedItemSelector(
|
||||||
items=items,
|
items = items,
|
||||||
stringify=lambda item: f"{item.name} ({item.isbn})",
|
stringify = lambda item: f"{item.name} ({item.isbn})",
|
||||||
)
|
)
|
||||||
selector.cmdloop()
|
selector.cmdloop()
|
||||||
if selector.result is not None:
|
if selector.result is not None:
|
||||||
self.result = selector.result
|
self.result = selector.result
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def do_search_author(self, _: str):
|
def do_search_author(self, _: str):
|
||||||
while (input_text := input("Enter author name: ")) == "":
|
while (input_text := input('Enter author name: ')) == '':
|
||||||
pass
|
pass
|
||||||
|
|
||||||
author = self.sql_session.scalars(
|
author = self.sql_session.scalars(
|
||||||
select(Author).where(Author.name.ilike(f"%{input_text}%")),
|
select(Author)
|
||||||
|
.where(Author.name.ilike(f'%{input_text}%')),
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
if len(author) == 0:
|
if len(author) == 0:
|
||||||
print("No authors found.")
|
print('No authors found.')
|
||||||
return
|
return
|
||||||
elif len(author) == 1:
|
elif len(author) == 1:
|
||||||
selected_author = author[0]
|
selected_author = author[0]
|
||||||
print("Found author:")
|
print('Found author:')
|
||||||
print(
|
print(f" {selected_author.name} ({sum(item.amount for item in selected_author.books)} items)")
|
||||||
f" {selected_author.name} ({sum(item.amount for item in selected_author.items)} items)"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
selector = NumberedItemSelector(
|
selector = NumberedItemSelector(
|
||||||
items=author,
|
items = author,
|
||||||
stringify=lambda author: f"{author.name} ({sum(item.amount for item in author.items)} items)",
|
stringify = lambda author: f"{author.name} ({sum(item.amount for item in author.books)} items)",
|
||||||
)
|
)
|
||||||
selector.cmdloop()
|
selector.cmdloop()
|
||||||
if selector.result is None:
|
if selector.result is None:
|
||||||
@@ -67,73 +69,77 @@ class SearchCli(NumberedCmd):
|
|||||||
selected_author = selector.result
|
selected_author = selector.result
|
||||||
|
|
||||||
selector = NumberedItemSelector(
|
selector = NumberedItemSelector(
|
||||||
items=list(selected_author.items),
|
items = selected_author.books,
|
||||||
stringify=lambda item: f"{item.name} ({item.isbn})",
|
stringify = lambda item: f"{item.name} ({item.isbn})",
|
||||||
)
|
)
|
||||||
selector.cmdloop()
|
selector.cmdloop()
|
||||||
if selector.result is not None:
|
if selector.result is not None:
|
||||||
self.result = selector.result
|
self.result = selector.result
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def do_search_owner(self, _: str):
|
def do_search_owner(self, _: str):
|
||||||
while (input_text := input("Enter username: ")) == "":
|
while (input_text := input('Enter username: ')) == '':
|
||||||
pass
|
pass
|
||||||
|
|
||||||
users = self.sql_session.scalars(
|
users = self.sql_session.scalars(
|
||||||
select(BookcaseItem.owner)
|
select(BookcaseItem.owner)
|
||||||
.where(BookcaseItem.owner.ilike(f"%{input_text}%"))
|
.where(BookcaseItem.owner.ilike(f'%{input_text}%'))
|
||||||
.distinct(),
|
.distinct(),
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
if len(users) == 0:
|
if len(users) == 0:
|
||||||
print("No users found.")
|
print('No users found.')
|
||||||
return
|
return
|
||||||
elif len(users) == 1:
|
elif len(users) == 1:
|
||||||
selected_user = users[0]
|
selected_user = users[0]
|
||||||
print("Found user:")
|
print('Found user:')
|
||||||
print(f" {selected_user}")
|
print(f" {selected_user}")
|
||||||
else:
|
else:
|
||||||
selector = NumberedItemSelector(items=users)
|
selector = NumberedItemSelector(items = users)
|
||||||
selector.cmdloop()
|
selector.cmdloop()
|
||||||
if selector.result is None:
|
if selector.result is None:
|
||||||
return
|
return
|
||||||
selected_user = selector.result
|
selected_user = selector.result
|
||||||
|
|
||||||
items = self.sql_session.scalars(
|
items = self.sql_session.scalars(
|
||||||
select(BookcaseItem).where(BookcaseItem.owner == selected_user),
|
select(BookcaseItem)
|
||||||
|
.where(BookcaseItem.owner == selected_user),
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
selector = NumberedItemSelector(
|
selector = NumberedItemSelector(
|
||||||
items=items,
|
items = items,
|
||||||
stringify=lambda item: f"{item.name} ({item.isbn})",
|
stringify = lambda item: f"{item.name} ({item.isbn})",
|
||||||
)
|
)
|
||||||
selector.cmdloop()
|
selector.cmdloop()
|
||||||
if selector.result is not None:
|
if selector.result is not None:
|
||||||
self.result = selector.result
|
self.result = selector.result
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def do_done(self, _: str):
|
def do_done(self, _: str):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
funcs = {
|
funcs = {
|
||||||
1: {
|
1: {
|
||||||
"f": do_search_all,
|
'f': do_search_all,
|
||||||
"doc": "Search everything",
|
'doc': 'Search everything',
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
"f": do_search_title,
|
'f': do_search_title,
|
||||||
"doc": "Search by title",
|
'doc': 'Search by title',
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
"f": do_search_author,
|
'f': do_search_author,
|
||||||
"doc": "Search by author",
|
'doc': 'Search by author',
|
||||||
},
|
},
|
||||||
4: {
|
4: {
|
||||||
"f": do_search_owner,
|
'f': do_search_owner,
|
||||||
"doc": "Search by owner",
|
'doc': 'Search by owner',
|
||||||
},
|
},
|
||||||
9: {
|
9: {
|
||||||
"f": do_done,
|
'f': do_done,
|
||||||
"doc": "Done",
|
'doc': 'Done',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
1
worblehat/deadline_daemon/__init__.py
Normal file
1
worblehat/deadline_daemon/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .main import DeadlineDaemon
|
||||||
147
worblehat/deadline_daemon/main.py
Normal file
147
worblehat/deadline_daemon/main.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.services.config import Config
|
||||||
|
from worblehat.models import (
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
DeadlineDaemonLastRunDatetime,
|
||||||
|
BookcaseItemBorrowingQueue,
|
||||||
|
)
|
||||||
|
from worblehat.services.email import send_email
|
||||||
|
|
||||||
|
class DeadlineDaemon:
|
||||||
|
def __init__(self, sql_session: Session):
|
||||||
|
self.sql_session = sql_session
|
||||||
|
self.last_run = self.sql_session.scalars(
|
||||||
|
select(DeadlineDaemonLastRunDatetime),
|
||||||
|
).one()
|
||||||
|
|
||||||
|
self.last_run_datetime = self.last_run.time
|
||||||
|
self.current_run_datetime = datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
logging.info('Deadline daemon started')
|
||||||
|
|
||||||
|
self.send_close_deadline_reminder_mails()
|
||||||
|
self.send_overdue_mails()
|
||||||
|
self.send_newly_available_mails()
|
||||||
|
self.send_expiring_queue_position_mails()
|
||||||
|
self.auto_expire_queue_positions()
|
||||||
|
|
||||||
|
self.last_run.time = self.current_run_datetime
|
||||||
|
self.sql_session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _sql_subtract_date(self, x: datetime, y: timedelta):
|
||||||
|
if self.sql_session.bind.dialect.name == 'sqlite':
|
||||||
|
# SQLite does not support timedelta in queries
|
||||||
|
return func.datetime(x, f'-{y.days} days')
|
||||||
|
elif self.sql_session.bind.dialect.name == 'postgresql':
|
||||||
|
return x - y
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f'Unsupported dialect: {self.sql_session.bind.dialect.name}')
|
||||||
|
|
||||||
|
|
||||||
|
def send_close_deadline_reminder_mails(self):
|
||||||
|
logging.info('Sending mails about items with a closing deadline')
|
||||||
|
|
||||||
|
# TODO: This should be int-parsed and validated before the daemon started
|
||||||
|
days = [int(d) for d in Config['deadline_daemon.warn_days_before_borrow_deadline']]
|
||||||
|
|
||||||
|
for day in days:
|
||||||
|
borrowings_to_remind = self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing)
|
||||||
|
.where(
|
||||||
|
self._sql_subtract_date(
|
||||||
|
BookcaseItemBorrowing.end_time,
|
||||||
|
timedelta(days=day),
|
||||||
|
)
|
||||||
|
.between(
|
||||||
|
self.last_run_datetime,
|
||||||
|
self.current_run_datetime,
|
||||||
|
),
|
||||||
|
BookcaseItemBorrowing.delivered.is_(None),
|
||||||
|
),
|
||||||
|
).all()
|
||||||
|
for borrowing in borrowings_to_remind:
|
||||||
|
logging.info(f' Sending close deadline mail to {borrowing.username}@pvv.ntnu.no. {day} days left')
|
||||||
|
send_email(
|
||||||
|
f'{borrowing.username}@pvv.ntnu.no',
|
||||||
|
'Reminder - Your borrowing deadline is approaching',
|
||||||
|
dedent(f'''
|
||||||
|
Your borrowing deadline for the following item is approaching:
|
||||||
|
|
||||||
|
{borrowing.item.name}
|
||||||
|
|
||||||
|
Please return the item by {borrowing.end_time.strftime("%a %b %d, %Y")}
|
||||||
|
''',
|
||||||
|
).strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_overdue_mails(self):
|
||||||
|
logging.info('Sending mails about overdue items')
|
||||||
|
|
||||||
|
to_remind = self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowing.end_time < self.current_run_datetime,
|
||||||
|
BookcaseItemBorrowing.delivered.is_(None),
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for borrowing in to_remind:
|
||||||
|
logging.info(f' Sending overdue mail to {borrowing.username}@pvv.ntnu.no for {borrowing.item.isbn} - {borrowing.end_time.strftime("%a %b %d, %Y")}')
|
||||||
|
send_email(
|
||||||
|
f'{borrowing.username}@pvv.ntnu.no',
|
||||||
|
'Your deadline has passed',
|
||||||
|
dedent(f'''
|
||||||
|
Your delivery deadline for the following item has passed:
|
||||||
|
|
||||||
|
{borrowing.item.name}
|
||||||
|
|
||||||
|
Please return the item as soon as possible.
|
||||||
|
''',
|
||||||
|
).strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_newly_available_mails(self):
|
||||||
|
logging.info('Sending mails about newly available items')
|
||||||
|
|
||||||
|
newly_available = self.sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue)
|
||||||
|
.join(
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseItemBorrowing.fk_bookcase_item_uid == BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowingQueue.expired.is_(False),
|
||||||
|
BookcaseItemBorrowing.delivered.is_not(None),
|
||||||
|
BookcaseItemBorrowing.delivered.between(
|
||||||
|
self.last_run_datetime,
|
||||||
|
self.current_run_datetime,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.order_by(BookcaseItemBorrowingQueue.entered_queue_time)
|
||||||
|
.group_by(BookcaseItemBorrowingQueue.fk_bookcase_item_uid)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for queue_item in newly_available:
|
||||||
|
logging.info(f'Sending newly available mail to {queue_item.username}')
|
||||||
|
logging.warning('Not implemented')
|
||||||
|
|
||||||
|
|
||||||
|
def send_expiring_queue_position_mails(self):
|
||||||
|
logging.info('Sending mails about queue positions which are expiring soon')
|
||||||
|
logging.warning('Not implemented')
|
||||||
|
|
||||||
|
|
||||||
|
def auto_expire_queue_positions(self):
|
||||||
|
logging.info('Expiring queue positions which are too old')
|
||||||
|
logging.warning('Not implemented')
|
||||||
432
worblehat/flaskapp/api/openapi.yaml
Normal file
432
worblehat/flaskapp/api/openapi.yaml
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: Worbblehat API Documentation
|
||||||
|
description: |-
|
||||||
|
This is the API documentation for the Worbblehat application.
|
||||||
|
It is based on the OpenAPI 3.0.3 specification.
|
||||||
|
The API is implemented using the Flask framework.
|
||||||
|
license:
|
||||||
|
name: MIT
|
||||||
|
version: 0.0.1
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: book
|
||||||
|
description: Everything about books
|
||||||
|
- name: bookshelf
|
||||||
|
description: Operations about bookshelves
|
||||||
|
- name: loan
|
||||||
|
description: Operations about loans
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/book/metadata/{isbn}:
|
||||||
|
get:
|
||||||
|
summary: Fetch a book metadata by ISBN
|
||||||
|
tags:
|
||||||
|
- book
|
||||||
|
parameters:
|
||||||
|
- name: isbn
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The ISBN of the book to retrieve
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The book with the specified ISBN
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/BookMetadata'
|
||||||
|
'404':
|
||||||
|
description: Book not found
|
||||||
|
|
||||||
|
/book/{isbn}:
|
||||||
|
get:
|
||||||
|
summary: Fetch a book from database by ISBN
|
||||||
|
tags:
|
||||||
|
- book
|
||||||
|
parameters:
|
||||||
|
- name: isbn
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The ISBN of the book to retrieve
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The book with the specified ISBN
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Book'
|
||||||
|
'404':
|
||||||
|
description: Book not found
|
||||||
|
post:
|
||||||
|
summary: Add a new book to the database
|
||||||
|
tags:
|
||||||
|
- book
|
||||||
|
requestBody:
|
||||||
|
description: The book to add
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Book'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: The book was successfully added
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Book'
|
||||||
|
'400':
|
||||||
|
description: Invalid input
|
||||||
|
|
||||||
|
/book/move/{from_shelf_name}/{to_shelf_name}:
|
||||||
|
post:
|
||||||
|
summary: Move a book from one bookshelf to another
|
||||||
|
tags:
|
||||||
|
- book
|
||||||
|
- bookshelf
|
||||||
|
parameters:
|
||||||
|
- name: from_shelf_name
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The name of the bookshelf to move the book from
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: to_shelf_name
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The name of the bookshelf to move the book to
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
description: The book to move
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Book'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The book was successfully moved
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Book'
|
||||||
|
'400':
|
||||||
|
description: Invalid input
|
||||||
|
'404':
|
||||||
|
description: Bookshelf not found
|
||||||
|
|
||||||
|
/bookshelf:
|
||||||
|
get:
|
||||||
|
summary: Fetch all bookshelves from database
|
||||||
|
tags:
|
||||||
|
- bookshelf
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: All bookshelves
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/BookcaseShelf'
|
||||||
|
|
||||||
|
/bookshelf/{shelf_name}:
|
||||||
|
get:
|
||||||
|
summary: Fetch a bookshelf from database by name
|
||||||
|
tags:
|
||||||
|
- bookshelf
|
||||||
|
parameters:
|
||||||
|
- name: shelf_name
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The name of the bookshelf to retrieve
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The bookshelf with the specified name
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/BookcaseShelf'
|
||||||
|
'404':
|
||||||
|
description: Bookshelf not found
|
||||||
|
post:
|
||||||
|
summary: Add a new bookshelf to the database
|
||||||
|
tags:
|
||||||
|
- bookshelf
|
||||||
|
requestBody:
|
||||||
|
description: The bookshelf to add
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/BookcaseShelf'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: The bookshelf was successfully added
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/BookcaseShelf'
|
||||||
|
'400':
|
||||||
|
description: Invalid input
|
||||||
|
|
||||||
|
/bookshelf/books/{shelf_name}:
|
||||||
|
get:
|
||||||
|
summary: Fetch all books from a bookshelf
|
||||||
|
tags:
|
||||||
|
- bookshelf
|
||||||
|
parameters:
|
||||||
|
- name: shelf_name
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The name of the bookshelf to retrieve
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The books on the specified bookshelf
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Book'
|
||||||
|
'404':
|
||||||
|
description: Bookshelf not found
|
||||||
|
|
||||||
|
/loan/{isbn}:
|
||||||
|
post:
|
||||||
|
summary: Borrow a book
|
||||||
|
tags:
|
||||||
|
- loan
|
||||||
|
parameters:
|
||||||
|
- name: isbn
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The ISBN of the book to borrow
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
description: The user who wants to borrow the book
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: The username of the user who wants to borrow the book
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
description: The email of the user who wants to borrow the book
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The book was successfully borrowed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
loan_id:
|
||||||
|
type: integer
|
||||||
|
description: The id of the loan
|
||||||
|
book:
|
||||||
|
$ref: '#/components/schemas/Book'
|
||||||
|
user:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: The username of the user who borrowed the book
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
description: The email of the user who borrowed the book
|
||||||
|
'400':
|
||||||
|
description: Invalid input
|
||||||
|
'404':
|
||||||
|
description: Book not found
|
||||||
|
'409':
|
||||||
|
description: Book already borrowed
|
||||||
|
delete:
|
||||||
|
summary: Return a book
|
||||||
|
tags:
|
||||||
|
- loan
|
||||||
|
parameters:
|
||||||
|
- name: isbn
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The ISBN of the book to return
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
description: The user who wants to return the book
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: The username of the user who wants to return the book
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
description: The email of the user who wants to return the book
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: The book was successfully returned
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
loan_id:
|
||||||
|
type: integer
|
||||||
|
description: The id of the loan
|
||||||
|
book:
|
||||||
|
$ref: '#/components/schemas/Book'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
BookMetadata:
|
||||||
|
description: metadata of a book
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
isbn:
|
||||||
|
type: string
|
||||||
|
description: The ISBN of the book.
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description: The title of the book.
|
||||||
|
authors:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: A set of authors names of the book.
|
||||||
|
language:
|
||||||
|
type: string
|
||||||
|
description: The language of the book.
|
||||||
|
publish_date:
|
||||||
|
type: integer
|
||||||
|
description: The publish date of the book.
|
||||||
|
num_pages:
|
||||||
|
type: integer
|
||||||
|
description: The number of pages in the book.
|
||||||
|
subjects:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: A set of subjects related to the book.
|
||||||
|
|
||||||
|
Author:
|
||||||
|
description: An author of a book
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the author.
|
||||||
|
Category:
|
||||||
|
description: A category of a book
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the category.
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: The description of the category.
|
||||||
|
Language:
|
||||||
|
description: A language of a book
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the language.
|
||||||
|
iso639_1_code:
|
||||||
|
type: string
|
||||||
|
description: The iso639-1 code of the language.
|
||||||
|
|
||||||
|
MediaType:
|
||||||
|
description: A media type of a book
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the media type.
|
||||||
|
|
||||||
|
Book:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
uid:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
isbn:
|
||||||
|
type: integer
|
||||||
|
owner:
|
||||||
|
type: string
|
||||||
|
amount:
|
||||||
|
type: integer
|
||||||
|
media_type:
|
||||||
|
$ref: '#/components/schemas/MediaType'
|
||||||
|
shelf:
|
||||||
|
$ref: '#/components/schemas/BookcaseShelf'
|
||||||
|
language:
|
||||||
|
$ref: '#/components/schemas/Language'
|
||||||
|
categories:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Category'
|
||||||
|
authors:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Author'
|
||||||
|
required:
|
||||||
|
- uid
|
||||||
|
- name
|
||||||
|
- isbn
|
||||||
|
- owner
|
||||||
|
- amount
|
||||||
|
|
||||||
|
BookcaseShelf:
|
||||||
|
description: A bookshelf, its location
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the bookshelf.
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: The description of the bookshelf. Like its location.
|
||||||
|
|
||||||
|
# not sure if we keep this
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: The username of the user.
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
description: The email of the user.
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
description: The role of the user.
|
||||||
|
loans:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
description: The loan id's of the user.
|
||||||
|
|
||||||
|
#maybe auth.
|
||||||
@@ -2,12 +2,10 @@ from flask import Blueprint, render_template
|
|||||||
|
|
||||||
main = Blueprint("main", __name__, template_folder="main")
|
main = Blueprint("main", __name__, template_folder="main")
|
||||||
|
|
||||||
|
@main.route('/')
|
||||||
@main.route("/")
|
|
||||||
def index():
|
def index():
|
||||||
return render_template("main/index.html")
|
return render_template("main/index.html")
|
||||||
|
|
||||||
|
|
||||||
@main.route("/login")
|
@main.route("/login")
|
||||||
def login():
|
def login():
|
||||||
return render_template("main/login.html")
|
return render_template("main/login.html")
|
||||||
@@ -10,19 +10,18 @@ from worblehat.services.config import Config
|
|||||||
from .blueprints.main import main
|
from .blueprints.main import main
|
||||||
from .database import db
|
from .database import db
|
||||||
|
|
||||||
|
|
||||||
def create_app(args: dict[str, any] | None = None):
|
def create_app(args: dict[str, any] | None = None):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
app.config.update(Config["flask"])
|
app.config.update(Config['flask'])
|
||||||
app.config.update(Config._config)
|
app.config.update(Config._config)
|
||||||
app.config["SQLALCHEMY_DATABASE_URI"] = Config.db_string()
|
app.config['SQLALCHEMY_DATABASE_URI'] = Config.db_string()
|
||||||
app.config["SQLALCHEMY_ECHO"] = Config["logging.debug_sql"]
|
app.config['SQLALCHEMY_ECHO'] = Config['logging.debug_sql']
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
if not inspect(db.engine).has_table("Bookcase"):
|
if not inspect(db.engine).has_table('Bookcase'):
|
||||||
Base.metadata.create_all(db.engine)
|
Base.metadata.create_all(db.engine)
|
||||||
seed_data()
|
seed_data()
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ def create_app(args: dict[str, any] | None = None):
|
|||||||
|
|
||||||
|
|
||||||
def configure_admin(app):
|
def configure_admin(app):
|
||||||
admin = Admin(app, name="Worblehat", template_mode="bootstrap3")
|
admin = Admin(app, name='Worblehat', template_mode='bootstrap3')
|
||||||
admin.add_view(ModelView(Author, db.session))
|
admin.add_view(ModelView(Author, db.session))
|
||||||
admin.add_view(ModelView(Bookcase, db.session))
|
admin.add_view(ModelView(Bookcase, db.session))
|
||||||
admin.add_view(ModelView(BookcaseItem, db.session))
|
admin.add_view(ModelView(BookcaseItem, db.session))
|
||||||
18
worblehat/flaskapp/wsgi_dev.py
Normal file
18
worblehat/flaskapp/wsgi_dev.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from werkzeug import run_simple
|
||||||
|
|
||||||
|
from worblehat.services.config import Config
|
||||||
|
|
||||||
|
from .flaskapp import create_app
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = create_app()
|
||||||
|
run_simple(
|
||||||
|
hostname = 'localhost',
|
||||||
|
port = 5000,
|
||||||
|
application = app,
|
||||||
|
use_debugger = True,
|
||||||
|
use_reloader = True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
from .flaskapp import create_app
|
from .flaskapp import create_app
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = create_app()
|
app = create_app()
|
||||||
app.run()
|
app.run()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
main()
|
||||||
73
worblehat/main.py
Normal file
73
worblehat/main.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import logging
|
||||||
|
from pprint import pformat
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from .services import (
|
||||||
|
Config,
|
||||||
|
arg_parser,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .deadline_daemon import DeadlineDaemon
|
||||||
|
from .cli import WorblehatCli
|
||||||
|
from .flaskapp.wsgi_dev import main as flask_dev_main
|
||||||
|
from .flaskapp.wsgi_prod import main as flask_prod_main
|
||||||
|
|
||||||
|
|
||||||
|
def _print_version() -> None:
|
||||||
|
from worblehat import __version__
|
||||||
|
print(f'Worblehat version {__version__}')
|
||||||
|
|
||||||
|
|
||||||
|
def _connect_to_database(**engine_args) -> Session:
|
||||||
|
try:
|
||||||
|
engine = create_engine(Config.db_string(), **engine_args)
|
||||||
|
sql_session = Session(engine)
|
||||||
|
except Exception as err:
|
||||||
|
print('Error: could not connect to database.')
|
||||||
|
print(err)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
print(f"Debug: Connected to database at '{Config.db_string()}'")
|
||||||
|
return sql_session
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = arg_parser.parse_args()
|
||||||
|
Config.load_configuration(vars(args))
|
||||||
|
|
||||||
|
if Config['logging.debug']:
|
||||||
|
logging.basicConfig(encoding='utf-8', level=logging.DEBUG)
|
||||||
|
else:
|
||||||
|
logging.basicConfig(encoding='utf-8', level=logging.INFO)
|
||||||
|
|
||||||
|
if args.version:
|
||||||
|
_print_version()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if args.print_config:
|
||||||
|
print(f'Configuration:\n{pformat(vars(args))}')
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if args.command == 'deadline-daemon':
|
||||||
|
sql_session = _connect_to_database(echo=Config['logging.debug_sql'])
|
||||||
|
DeadlineDaemon(sql_session).run()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if args.command == 'cli':
|
||||||
|
sql_session = _connect_to_database(echo=Config['logging.debug_sql'])
|
||||||
|
WorblehatCli.run_with_safe_exit_wrapper(sql_session)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if args.command == 'flask-dev':
|
||||||
|
flask_dev_main()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if args.command == 'flask-prod':
|
||||||
|
if Config['logging.debug'] or Config['logging.debug_sql']:
|
||||||
|
logging.warn('Debug mode is enabled for the production server. This is not recommended.')
|
||||||
|
flask_prod_main()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
print(arg_parser.format_help())
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
Integer,
|
||||||
|
ForeignKey,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
|
mapped_column,
|
||||||
relationship,
|
relationship,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,11 +21,10 @@ from .xref_tables import Item_Author
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .BookcaseItem import BookcaseItem
|
from .BookcaseItem import BookcaseItem
|
||||||
|
|
||||||
|
|
||||||
class Author(Base, UidMixin, UniqueNameMixin):
|
class Author(Base, UidMixin, UniqueNameMixin):
|
||||||
items: Mapped[set[BookcaseItem]] = relationship(
|
items: Mapped[set[BookcaseItem]] = relationship(
|
||||||
secondary=Item_Author.__table__,
|
secondary = Item_Author.__table__,
|
||||||
back_populates="authors",
|
back_populates = 'authors',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -9,7 +9,6 @@ from sqlalchemy.orm.collections import (
|
|||||||
InstrumentedSet,
|
InstrumentedSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
metadata = MetaData(
|
metadata = MetaData(
|
||||||
naming_convention={
|
naming_convention={
|
||||||
@@ -17,7 +16,7 @@ class Base(DeclarativeBase):
|
|||||||
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,18 +26,15 @@ class Base(DeclarativeBase):
|
|||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
columns = ", ".join(
|
columns = ", ".join(
|
||||||
f"{k}={repr(v)}"
|
f"{k}={repr(v)}" for k, v in self.__dict__.items() if not any([
|
||||||
for k, v in self.__dict__.items()
|
|
||||||
if not any(
|
|
||||||
[
|
|
||||||
k.startswith("_"),
|
k.startswith("_"),
|
||||||
|
|
||||||
# Ensure that we don't try to print out the entire list of
|
# Ensure that we don't try to print out the entire list of
|
||||||
# relationships, which could create an infinite loop
|
# relationships, which could create an infinite loop
|
||||||
isinstance(v, Base),
|
isinstance(v, Base),
|
||||||
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})>"
|
||||||
@@ -13,15 +13,13 @@ from .mixins import (
|
|||||||
UidMixin,
|
UidMixin,
|
||||||
UniqueNameMixin,
|
UniqueNameMixin,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .BookcaseShelf import BookcaseShelf
|
from .BookcaseShelf import BookcaseShelf
|
||||||
|
|
||||||
|
|
||||||
class Bookcase(Base, UidMixin, UniqueNameMixin):
|
class Bookcase(Base, UidMixin, UniqueNameMixin):
|
||||||
description: Mapped[str | None] = mapped_column(Text)
|
description: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
||||||
shelfs: Mapped[list[BookcaseShelf]] = relationship(back_populates="bookcase")
|
shelfs: Mapped[list[BookcaseShelf]] = relationship(back_populates='bookcase')
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -34,5 +32,6 @@ class Bookcase(Base, UidMixin, UniqueNameMixin):
|
|||||||
def short_str(self) -> str:
|
def short_str(self) -> str:
|
||||||
result = self.name
|
result = self.name
|
||||||
if self.description is not None:
|
if self.description is not None:
|
||||||
result += f" [{self.description}]"
|
result += f' [{self.description}]'
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -2,10 +2,10 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
ForeignKey,
|
Integer,
|
||||||
SmallInteger,
|
SmallInteger,
|
||||||
String,
|
String,
|
||||||
Text,
|
ForeignKey,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
@@ -16,12 +16,12 @@ from sqlalchemy.orm import (
|
|||||||
from .Base import Base
|
from .Base import Base
|
||||||
from .mixins import (
|
from .mixins import (
|
||||||
UidMixin,
|
UidMixin,
|
||||||
|
UniqueNameMixin,
|
||||||
)
|
)
|
||||||
from .xref_tables import (
|
from .xref_tables import (
|
||||||
Item_Category,
|
Item_Category,
|
||||||
Item_Author,
|
Item_Author,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .Author import Author
|
from .Author import Author
|
||||||
from .BookcaseItemBorrowing import BookcaseItemBorrowing
|
from .BookcaseItemBorrowing import BookcaseItemBorrowing
|
||||||
@@ -31,51 +31,36 @@ if TYPE_CHECKING:
|
|||||||
from .Language import Language
|
from .Language import Language
|
||||||
from .MediaType import MediaType
|
from .MediaType import MediaType
|
||||||
|
|
||||||
from worblehat.flaskapp.database import db
|
class BookcaseItem(Base, UidMixin, UniqueNameMixin):
|
||||||
|
|
||||||
|
|
||||||
class BookcaseItem(Base, UidMixin):
|
|
||||||
isbn: Mapped[int] = mapped_column(String, unique=True, index=True)
|
isbn: Mapped[int] = mapped_column(String, unique=True, index=True)
|
||||||
name: Mapped[str] = mapped_column(Text, index=True)
|
owner: Mapped[str] = mapped_column(String, default='PVV')
|
||||||
owner: Mapped[str] = mapped_column(String, default="PVV")
|
|
||||||
amount: Mapped[int] = mapped_column(SmallInteger, default=1)
|
amount: Mapped[int] = mapped_column(SmallInteger, default=1)
|
||||||
|
|
||||||
fk_media_type_uid: Mapped[int] = mapped_column(ForeignKey("MediaType.uid"))
|
fk_media_type_uid: Mapped[int] = mapped_column(ForeignKey('MediaType.uid'))
|
||||||
fk_bookcase_shelf_uid: Mapped[int] = mapped_column(ForeignKey("BookcaseShelf.uid"))
|
fk_bookcase_shelf_uid: Mapped[int | None] = mapped_column(ForeignKey('BookcaseShelf.uid'))
|
||||||
fk_language_uid: Mapped[int | None] = mapped_column(ForeignKey("Language.uid"))
|
fk_language_uid: Mapped[int | None] = mapped_column(ForeignKey('Language.uid'))
|
||||||
|
|
||||||
media_type: Mapped[MediaType] = relationship(back_populates="items")
|
media_type: Mapped[MediaType] = relationship(back_populates='items')
|
||||||
shelf: Mapped[BookcaseShelf] = relationship(back_populates="items")
|
shelf: Mapped[BookcaseShelf] = relationship(back_populates='items')
|
||||||
language: Mapped[Language] = relationship()
|
language: Mapped[Language] = relationship()
|
||||||
borrowings: Mapped[set[BookcaseItemBorrowing]] = relationship(back_populates="item")
|
borrowings: Mapped[set[BookcaseItemBorrowing]] = relationship(back_populates='item')
|
||||||
borrowing_queue: Mapped[set[BookcaseItemBorrowingQueue]] = relationship(
|
borrowing_queue: Mapped[set[BookcaseItemBorrowingQueue]] = relationship(back_populates='item')
|
||||||
back_populates="item"
|
|
||||||
)
|
|
||||||
|
|
||||||
categories: Mapped[set[Category]] = relationship(
|
categories: Mapped[set[Category]] = relationship(
|
||||||
secondary=Item_Category.__table__,
|
secondary = Item_Category.__table__,
|
||||||
back_populates="items",
|
back_populates = 'items',
|
||||||
)
|
)
|
||||||
authors: Mapped[set[Author]] = relationship(
|
authors: Mapped[set[Author]] = relationship(
|
||||||
secondary=Item_Author.__table__,
|
secondary = Item_Author.__table__,
|
||||||
back_populates="items",
|
back_populates = 'items',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
isbn: int | None = None,
|
isbn: int | None = None,
|
||||||
owner: str = "PVV",
|
owner: str = 'PVV',
|
||||||
):
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.isbn = isbn
|
self.isbn = isbn
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_isbn(cls, isbn: str, sql_session: Session = db.session) -> Self | None:
|
|
||||||
"""
|
|
||||||
NOTE:
|
|
||||||
This method defaults to using the flask_sqlalchemy session.
|
|
||||||
It will not work outside of a request context, unless another session is provided.
|
|
||||||
"""
|
|
||||||
return sql_session.query(cls).where(cls.isbn == isbn).one_or_none()
|
|
||||||
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
|
Boolean,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
String,
|
String,
|
||||||
DateTime,
|
DateTime,
|
||||||
@@ -15,24 +16,18 @@ from sqlalchemy.orm import (
|
|||||||
|
|
||||||
from .Base import Base
|
from .Base import Base
|
||||||
from .mixins import UidMixin
|
from .mixins import UidMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .BookcaseItem import BookcaseItem
|
from .BookcaseItem import BookcaseItem
|
||||||
|
|
||||||
|
|
||||||
class BookcaseItemBorrowing(Base, UidMixin):
|
class BookcaseItemBorrowing(Base, UidMixin):
|
||||||
username: Mapped[str] = mapped_column(String)
|
username: Mapped[str] = mapped_column(String)
|
||||||
start_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
start_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
||||||
end_time: Mapped[datetime] = mapped_column(
|
end_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now() + timedelta(days=30))
|
||||||
DateTime, default=datetime.now() + timedelta(days=30)
|
|
||||||
)
|
|
||||||
delivered: Mapped[datetime | None] = mapped_column(DateTime, default=None)
|
delivered: Mapped[datetime | None] = mapped_column(DateTime, default=None)
|
||||||
|
|
||||||
fk_bookcase_item_uid: Mapped[int] = mapped_column(
|
fk_bookcase_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), index=True)
|
||||||
ForeignKey("BookcaseItem.uid"), index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
item: Mapped[BookcaseItem] = relationship(back_populates="borrowings")
|
item: Mapped[BookcaseItem] = relationship(back_populates='borrowings')
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -16,24 +16,17 @@ from sqlalchemy.orm import (
|
|||||||
|
|
||||||
from .Base import Base
|
from .Base import Base
|
||||||
from .mixins import UidMixin
|
from .mixins import UidMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .BookcaseItem import BookcaseItem
|
from .BookcaseItem import BookcaseItem
|
||||||
|
|
||||||
|
|
||||||
class BookcaseItemBorrowingQueue(Base, UidMixin):
|
class BookcaseItemBorrowingQueue(Base, UidMixin):
|
||||||
username: Mapped[str] = mapped_column(String)
|
username: Mapped[str] = mapped_column(String)
|
||||||
entered_queue_time: Mapped[datetime] = mapped_column(
|
entered_queue_time = mapped_column(DateTime, default=datetime.now())
|
||||||
DateTime, default=datetime.now()
|
|
||||||
)
|
|
||||||
item_became_available_time: Mapped[datetime | None] = mapped_column(DateTime)
|
|
||||||
expired = mapped_column(Boolean, default=False)
|
expired = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
fk_bookcase_item_uid: Mapped[int] = mapped_column(
|
fk_bookcase_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), index=True)
|
||||||
ForeignKey("BookcaseItem.uid"), index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
item: Mapped[BookcaseItem] = relationship(back_populates="borrowing_queue")
|
item: Mapped[BookcaseItem] = relationship(back_populates='borrowing_queue')
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
|
Integer,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
SmallInteger,
|
SmallInteger,
|
||||||
Text,
|
Text,
|
||||||
@@ -15,7 +16,6 @@ from sqlalchemy.orm import (
|
|||||||
|
|
||||||
from .Base import Base
|
from .Base import Base
|
||||||
from .mixins import UidMixin
|
from .mixins import UidMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .Bookcase import Bookcase
|
from .Bookcase import Bookcase
|
||||||
from .BookcaseItem import BookcaseItem
|
from .BookcaseItem import BookcaseItem
|
||||||
@@ -23,23 +23,22 @@ if TYPE_CHECKING:
|
|||||||
# NOTE: Booshelfs are 0 indexed for both rows and columns,
|
# NOTE: Booshelfs are 0 indexed for both rows and columns,
|
||||||
# where cell 0-0 is placed in the lower right corner.
|
# where cell 0-0 is placed in the lower right corner.
|
||||||
|
|
||||||
|
|
||||||
class BookcaseShelf(Base, UidMixin):
|
class BookcaseShelf(Base, UidMixin):
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint(
|
UniqueConstraint(
|
||||||
"column",
|
'column',
|
||||||
"fk_bookcase_uid",
|
'fk_bookcase_uid',
|
||||||
"row",
|
'row',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
description: Mapped[str | None] = mapped_column(Text)
|
description: Mapped[str | None] = mapped_column(Text)
|
||||||
row: Mapped[int] = mapped_column(SmallInteger)
|
row: Mapped[int] = mapped_column(SmallInteger)
|
||||||
column: Mapped[int] = mapped_column(SmallInteger)
|
column: Mapped[int] = mapped_column(SmallInteger)
|
||||||
|
|
||||||
fk_bookcase_uid: Mapped[int] = mapped_column(ForeignKey("Bookcase.uid"))
|
fk_bookcase_uid: Mapped[int] = mapped_column(ForeignKey('Bookcase.uid'))
|
||||||
|
|
||||||
bookcase: Mapped[Bookcase] = relationship(back_populates="shelfs")
|
bookcase: Mapped[Bookcase] = relationship(back_populates='shelfs')
|
||||||
items: Mapped[set[BookcaseItem]] = relationship(back_populates="shelf")
|
items: Mapped[set[BookcaseItem]] = relationship(back_populates='shelf')
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -54,7 +53,7 @@ class BookcaseShelf(Base, UidMixin):
|
|||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
def short_str(self) -> str:
|
def short_str(self) -> str:
|
||||||
result = f"{self.column}-{self.row}"
|
result = f'{self.column}-{self.row}'
|
||||||
if self.description is not None:
|
if self.description is not None:
|
||||||
result += f" [{self.description}]"
|
result += f' [{self.description}]'
|
||||||
return result
|
return result
|
||||||
@@ -14,17 +14,15 @@ from .mixins import (
|
|||||||
UniqueNameMixin,
|
UniqueNameMixin,
|
||||||
)
|
)
|
||||||
from .xref_tables import Item_Category
|
from .xref_tables import Item_Category
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .BookcaseItem import BookcaseItem
|
from .BookcaseItem import BookcaseItem
|
||||||
|
|
||||||
|
|
||||||
class Category(Base, UidMixin, UniqueNameMixin):
|
class Category(Base, UidMixin, UniqueNameMixin):
|
||||||
description: Mapped[str | None] = mapped_column(Text)
|
description: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
||||||
items: Mapped[set[BookcaseItem]] = relationship(
|
items: Mapped[set[BookcaseItem]] = relationship(
|
||||||
secondary=Item_Category.__table__,
|
secondary=Item_Category.__table__,
|
||||||
back_populates="categories",
|
back_populates='categories',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -12,17 +12,12 @@ from sqlalchemy.orm import (
|
|||||||
|
|
||||||
from .Base import Base
|
from .Base import Base
|
||||||
|
|
||||||
|
|
||||||
class DeadlineDaemonLastRunDatetime(Base):
|
class DeadlineDaemonLastRunDatetime(Base):
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
CheckConstraint(
|
CheckConstraint(
|
||||||
"uid = true",
|
'uid = true',
|
||||||
name="single_row_only",
|
name = 'single_row_only',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
uid: Mapped[bool] = mapped_column(Boolean, primary_key=True, default=True)
|
uid: Mapped[bool] = mapped_column(Boolean, primary_key=True, default=True)
|
||||||
time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
||||||
|
|
||||||
def __init__(self, time: datetime | None = None):
|
|
||||||
if time is not None:
|
|
||||||
self.time = time
|
|
||||||
@@ -11,7 +11,6 @@ from sqlalchemy.orm import (
|
|||||||
from .Base import Base
|
from .Base import Base
|
||||||
from .mixins import UidMixin, UniqueNameMixin
|
from .mixins import UidMixin, UniqueNameMixin
|
||||||
|
|
||||||
|
|
||||||
class Language(Base, UidMixin, UniqueNameMixin):
|
class Language(Base, UidMixin, UniqueNameMixin):
|
||||||
iso639_1_code: Mapped[str] = mapped_column(String(2), unique=True, index=True)
|
iso639_1_code: Mapped[str] = mapped_column(String(2), unique=True, index=True)
|
||||||
|
|
||||||
@@ -10,15 +10,13 @@ from sqlalchemy.orm import (
|
|||||||
|
|
||||||
from .Base import Base
|
from .Base import Base
|
||||||
from .mixins import UidMixin, UniqueNameMixin
|
from .mixins import UidMixin, UniqueNameMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .BookcaseItem import BookcaseItem
|
from .BookcaseItem import BookcaseItem
|
||||||
|
|
||||||
|
|
||||||
class MediaType(Base, UidMixin, UniqueNameMixin):
|
class MediaType(Base, UidMixin, UniqueNameMixin):
|
||||||
description: Mapped[str | None] = mapped_column(Text)
|
description: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|
||||||
items: Mapped[set[BookcaseItem]] = relationship(back_populates="media_type")
|
items: Mapped[set[BookcaseItem]] = relationship(back_populates='media_type')
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -27,3 +25,5 @@ class MediaType(Base, UidMixin, UniqueNameMixin):
|
|||||||
):
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
|
|
||||||
@@ -9,17 +9,3 @@ from .Category import Category
|
|||||||
from .DeadlineDaemonLastRunDatetime import DeadlineDaemonLastRunDatetime
|
from .DeadlineDaemonLastRunDatetime import DeadlineDaemonLastRunDatetime
|
||||||
from .Language import Language
|
from .Language import Language
|
||||||
from .MediaType import MediaType
|
from .MediaType import MediaType
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"Author",
|
|
||||||
"Base",
|
|
||||||
"Bookcase",
|
|
||||||
"BookcaseItem",
|
|
||||||
"BookcaseItemBorrowing",
|
|
||||||
"BookcaseItemBorrowingQueue",
|
|
||||||
"BookcaseShelf",
|
|
||||||
"Category",
|
|
||||||
"DeadlineDaemonLastRunDatetime",
|
|
||||||
"Language",
|
|
||||||
"MediaType",
|
|
||||||
]
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from alembic import context
|
from alembic import context
|
||||||
|
from flask import current_app
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
from sqlalchemy import engine_from_config
|
from sqlalchemy import engine_from_config
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
@@ -11,14 +12,9 @@ config = context.config
|
|||||||
if config.config_file_name is not None:
|
if config.config_file_name is not None:
|
||||||
fileConfig(config.config_file_name)
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
config_attrs = {}
|
Config.load_configuration({})
|
||||||
if (config_path := context.get_x_argument(as_dictionary=True).get('config', None)):
|
|
||||||
config_attrs['config_file'] = config_path
|
|
||||||
|
|
||||||
Config.load_configuration(config_attrs)
|
|
||||||
|
|
||||||
config.set_main_option("sqlalchemy.url", Config.db_string())
|
|
||||||
|
|
||||||
|
config.set_main_option('sqlalchemy.url', Config.db_string())
|
||||||
|
|
||||||
# This will make sure alembic doesn't generate empty migrations
|
# This will make sure alembic doesn't generate empty migrations
|
||||||
# https://stackoverflow.com/questions/70203927/how-to-prevent-alembic-revision-autogenerate-from-making-revision-file-if-it-h
|
# https://stackoverflow.com/questions/70203927/how-to-prevent-alembic-revision-autogenerate-from-making-revision-file-if-it-h
|
||||||
@@ -27,8 +23,7 @@ def _process_revision_directives(context, revision, directives):
|
|||||||
script = directives[0]
|
script = directives[0]
|
||||||
if script.upgrade_ops.is_empty():
|
if script.upgrade_ops.is_empty():
|
||||||
directives[:] = []
|
directives[:] = []
|
||||||
print("No changes in schema detected. Not generating migration.")
|
print('No changes in schema detected. Not generating migration.')
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online() -> None:
|
def run_migrations_online() -> None:
|
||||||
connectable = engine_from_config(
|
connectable = engine_from_config(
|
||||||
@@ -41,9 +36,11 @@ def run_migrations_online() -> None:
|
|||||||
context.configure(
|
context.configure(
|
||||||
connection=connection,
|
connection=connection,
|
||||||
target_metadata=Base.metadata,
|
target_metadata=Base.metadata,
|
||||||
|
|
||||||
# Extended type checking with alembic when generating migrations
|
# Extended type checking with alembic when generating migrations
|
||||||
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect
|
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect
|
||||||
compare_type=True,
|
compare_type=True,
|
||||||
|
|
||||||
# This is required for ALTER TABLE to work with sqlite.
|
# This is required for ALTER TABLE to work with sqlite.
|
||||||
# It should have no effect on postgreSQL
|
# It should have no effect on postgreSQL
|
||||||
# https://alembic.sqlalchemy.org/en/latest/batch.html
|
# https://alembic.sqlalchemy.org/en/latest/batch.html
|
||||||
@@ -54,7 +51,6 @@ def run_migrations_online() -> None:
|
|||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
# We don't have any good reasons to generate raw sql migrations,
|
# We don't have any good reasons to generate raw sql migrations,
|
||||||
# so the `run_migrations_offline` has been removed
|
# so the `run_migrations_offline` has been removed
|
||||||
run_migrations_online()
|
run_migrations_online()
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
"""initial_migration
|
||||||
|
|
||||||
|
Revision ID: d51c7172d2f2
|
||||||
|
Revises:
|
||||||
|
Create Date: 2023-05-06 17:46:39.230122
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd51c7172d2f2'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('Author',
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.Text(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('uid', name=op.f('pk_Author'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('Author', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_Author_name'), ['name'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('Bookcase',
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.Text(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('uid', name=op.f('pk_Bookcase'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('Bookcase', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_Bookcase_name'), ['name'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('Category',
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.Text(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('uid', name=op.f('pk_Category'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('Category', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_Category_name'), ['name'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('Language',
|
||||||
|
sa.Column('iso639_1_code', sa.String(length=2), nullable=False),
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.Text(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('uid', name=op.f('pk_Language'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('Language', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_Language_iso639_1_code'), ['iso639_1_code'], unique=True)
|
||||||
|
batch_op.create_index(batch_op.f('ix_Language_name'), ['name'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('MediaType',
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.Text(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('uid', name=op.f('pk_MediaType'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('MediaType', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_MediaType_name'), ['name'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('BookcaseShelf',
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('row', sa.SmallInteger(), nullable=False),
|
||||||
|
sa.Column('column', sa.SmallInteger(), nullable=False),
|
||||||
|
sa.Column('fk_bookcase_uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['fk_bookcase_uid'], ['Bookcase.uid'], name=op.f('fk_BookcaseShelf_fk_bookcase_uid_Bookcase')),
|
||||||
|
sa.PrimaryKeyConstraint('uid', name=op.f('pk_BookcaseShelf')),
|
||||||
|
sa.UniqueConstraint('column', 'fk_bookcase_uid', 'row', name=op.f('uq_BookcaseShelf_column'))
|
||||||
|
)
|
||||||
|
op.create_table('BookcaseItem',
|
||||||
|
sa.Column('isbn', sa.String(), nullable=False),
|
||||||
|
sa.Column('owner', sa.String(), nullable=False),
|
||||||
|
sa.Column('amount', sa.SmallInteger(), nullable=False),
|
||||||
|
sa.Column('fk_media_type_uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('fk_bookcase_shelf_uid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('fk_language_uid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.Text(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['fk_bookcase_shelf_uid'], ['BookcaseShelf.uid'], name=op.f('fk_BookcaseItem_fk_bookcase_shelf_uid_BookcaseShelf')),
|
||||||
|
sa.ForeignKeyConstraint(['fk_language_uid'], ['Language.uid'], name=op.f('fk_BookcaseItem_fk_language_uid_Language')),
|
||||||
|
sa.ForeignKeyConstraint(['fk_media_type_uid'], ['MediaType.uid'], name=op.f('fk_BookcaseItem_fk_media_type_uid_MediaType')),
|
||||||
|
sa.PrimaryKeyConstraint('uid', name=op.f('pk_BookcaseItem'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('BookcaseItem', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_BookcaseItem_isbn'), ['isbn'], unique=True)
|
||||||
|
batch_op.create_index(batch_op.f('ix_BookcaseItem_name'), ['name'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('BookcaseItemBorrowing',
|
||||||
|
sa.Column('username', sa.String(), nullable=False),
|
||||||
|
sa.Column('start_time', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('end_time', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('delivered', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('fk_bookcase_item_uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['fk_bookcase_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_BookcaseItemBorrowing_fk_bookcase_item_uid_BookcaseItem')),
|
||||||
|
sa.PrimaryKeyConstraint('uid', name=op.f('pk_BookcaseItemBorrowing'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('BookcaseItemBorrowing', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_BookcaseItemBorrowing_fk_bookcase_item_uid'), ['fk_bookcase_item_uid'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('BookcaseItemBorrowingQueue',
|
||||||
|
sa.Column('username', sa.String(), nullable=False),
|
||||||
|
sa.Column('entered_queue_time', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('should_notify_user', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('fk_bookcase_item_uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['fk_bookcase_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_BookcaseItemBorrowingQueue_fk_bookcase_item_uid_BookcaseItem')),
|
||||||
|
sa.PrimaryKeyConstraint('uid', name=op.f('pk_BookcaseItemBorrowingQueue'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('BookcaseItemBorrowingQueue', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_BookcaseItemBorrowingQueue_fk_bookcase_item_uid'), ['fk_bookcase_item_uid'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('Item_Author',
|
||||||
|
sa.Column('fk_item_uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('fk_author_uid', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['fk_author_uid'], ['Author.uid'], name=op.f('fk_Item_Author_fk_author_uid_Author')),
|
||||||
|
sa.ForeignKeyConstraint(['fk_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_Item_Author_fk_item_uid_BookcaseItem')),
|
||||||
|
sa.PrimaryKeyConstraint('fk_item_uid', 'fk_author_uid', name=op.f('pk_Item_Author'))
|
||||||
|
)
|
||||||
|
op.create_table('Item_Category',
|
||||||
|
sa.Column('fk_item_uid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('fk_category_uid', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['fk_category_uid'], ['Category.uid'], name=op.f('fk_Item_Category_fk_category_uid_Category')),
|
||||||
|
sa.ForeignKeyConstraint(['fk_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_Item_Category_fk_item_uid_BookcaseItem')),
|
||||||
|
sa.PrimaryKeyConstraint('fk_item_uid', 'fk_category_uid', name=op.f('pk_Item_Category'))
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('Item_Category')
|
||||||
|
op.drop_table('Item_Author')
|
||||||
|
with op.batch_alter_table('BookcaseItemBorrowingQueue', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_BookcaseItemBorrowingQueue_fk_bookcase_item_uid'))
|
||||||
|
|
||||||
|
op.drop_table('BookcaseItemBorrowingQueue')
|
||||||
|
with op.batch_alter_table('BookcaseItemBorrowing', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_BookcaseItemBorrowing_fk_bookcase_item_uid'))
|
||||||
|
|
||||||
|
op.drop_table('BookcaseItemBorrowing')
|
||||||
|
with op.batch_alter_table('BookcaseItem', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_BookcaseItem_name'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_BookcaseItem_isbn'))
|
||||||
|
|
||||||
|
op.drop_table('BookcaseItem')
|
||||||
|
op.drop_table('BookcaseShelf')
|
||||||
|
with op.batch_alter_table('MediaType', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_MediaType_name'))
|
||||||
|
|
||||||
|
op.drop_table('MediaType')
|
||||||
|
with op.batch_alter_table('Language', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_Language_name'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_Language_iso639_1_code'))
|
||||||
|
|
||||||
|
op.drop_table('Language')
|
||||||
|
with op.batch_alter_table('Category', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_Category_name'))
|
||||||
|
|
||||||
|
op.drop_table('Category')
|
||||||
|
with op.batch_alter_table('Bookcase', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_Bookcase_name'))
|
||||||
|
|
||||||
|
op.drop_table('Bookcase')
|
||||||
|
with op.batch_alter_table('Author', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_Author_name'))
|
||||||
|
|
||||||
|
op.drop_table('Author')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -9,7 +9,6 @@ from sqlalchemy.orm import (
|
|||||||
|
|
||||||
from worblehat.flaskapp.database import db
|
from worblehat.flaskapp.database import db
|
||||||
|
|
||||||
|
|
||||||
class UidMixin(object):
|
class UidMixin(object):
|
||||||
uid: Mapped[int] = mapped_column(Integer, primary_key=True)
|
uid: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
|
||||||
@@ -9,7 +9,6 @@ from sqlalchemy.orm import (
|
|||||||
|
|
||||||
from worblehat.flaskapp.database import db
|
from worblehat.flaskapp.database import db
|
||||||
|
|
||||||
|
|
||||||
class UniqueNameMixin(object):
|
class UniqueNameMixin(object):
|
||||||
name: Mapped[str] = mapped_column(Text, unique=True, index=True)
|
name: Mapped[str] = mapped_column(Text, unique=True, index=True)
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
from sqlalchemy.orm import declared_attr
|
from sqlalchemy.orm import declared_attr
|
||||||
|
|
||||||
|
|
||||||
class XrefMixin(object):
|
class XrefMixin(object):
|
||||||
@declared_attr.directive
|
@declared_attr.directive
|
||||||
def __tablename__(cls) -> str:
|
def __tablename__(cls) -> str:
|
||||||
return f"xref_{cls.__name__.lower()}"
|
return f'xref_{cls.__name__.lower()}'
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
from .UidMixin import UidMixin
|
from .UidMixin import UidMixin
|
||||||
from .UniqueNameMixin import UniqueNameMixin
|
from .UniqueNameMixin import UniqueNameMixin
|
||||||
|
|
||||||
__all__ = ["UidMixin", "UniqueNameMixin"]
|
|
||||||
15
worblehat/models/xref_tables/Item_Author.py
Normal file
15
worblehat/models/xref_tables/Item_Author.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Integer,
|
||||||
|
ForeignKey,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import (
|
||||||
|
Mapped,
|
||||||
|
mapped_column,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..Base import Base
|
||||||
|
from ..mixins.XrefMixin import XrefMixin
|
||||||
|
|
||||||
|
class Item_Author(Base, XrefMixin):
|
||||||
|
fk_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), primary_key=True)
|
||||||
|
fk_author_uid: Mapped[int] = mapped_column(ForeignKey('Author.uid'), primary_key=True)
|
||||||
15
worblehat/models/xref_tables/Item_Category.py
Normal file
15
worblehat/models/xref_tables/Item_Category.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from sqlalchemy import (
|
||||||
|
Integer,
|
||||||
|
ForeignKey,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import (
|
||||||
|
Mapped,
|
||||||
|
mapped_column,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..Base import Base
|
||||||
|
from ..mixins.XrefMixin import XrefMixin
|
||||||
|
|
||||||
|
class Item_Category(Base, XrefMixin):
|
||||||
|
fk_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), primary_key=True)
|
||||||
|
fk_category_uid: Mapped[int] = mapped_column(ForeignKey('Category.uid'), primary_key=True)
|
||||||
2
worblehat/models/xref_tables/__init__.py
Normal file
2
worblehat/models/xref_tables/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .Item_Author import Item_Author
|
||||||
|
from .Item_Category import Item_Category
|
||||||
8
worblehat/services/__init__.py
Normal file
8
worblehat/services/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from .argument_parser import arg_parser
|
||||||
|
from .bookcase_item import (
|
||||||
|
create_bookcase_item_from_isbn,
|
||||||
|
is_valid_isbn,
|
||||||
|
)
|
||||||
|
from .config import Config
|
||||||
|
from .email import send_email
|
||||||
|
from .seed_test_data import seed_data
|
||||||
53
worblehat/services/argument_parser.py
Normal file
53
worblehat/services/argument_parser.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from argparse import ArgumentParser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def _is_valid_file(parser: ArgumentParser, arg: str) -> Path:
|
||||||
|
path = Path(arg)
|
||||||
|
if not path.is_file():
|
||||||
|
parser.error(f'The file {arg} does not exist!')
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
arg_parser = ArgumentParser(
|
||||||
|
description = 'Worblehat library management system',
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers = arg_parser.add_subparsers(dest='command')
|
||||||
|
subparsers.add_parser(
|
||||||
|
'deadline-daemon',
|
||||||
|
help = 'Initialize a single pass of the daemon which sends deadline emails',
|
||||||
|
)
|
||||||
|
subparsers.add_parser(
|
||||||
|
'cli',
|
||||||
|
help = 'Start the command line interface',
|
||||||
|
)
|
||||||
|
subparsers.add_parser(
|
||||||
|
'flask-dev',
|
||||||
|
help = 'Start the web interface in development mode',
|
||||||
|
)
|
||||||
|
subparsers.add_parser(
|
||||||
|
'flask-prod',
|
||||||
|
help = 'Start the web interface in production mode',
|
||||||
|
)
|
||||||
|
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'-V',
|
||||||
|
'--version',
|
||||||
|
action = 'store_true',
|
||||||
|
help = 'Print version and exit',
|
||||||
|
)
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'-c',
|
||||||
|
'--config',
|
||||||
|
type=lambda x: _is_valid_file(arg_parser, x),
|
||||||
|
help = 'Path to config file',
|
||||||
|
dest = 'config_file',
|
||||||
|
metavar = 'FILE',
|
||||||
|
)
|
||||||
|
arg_parser.add_argument(
|
||||||
|
'-p',
|
||||||
|
'--print-config',
|
||||||
|
action = 'store_true',
|
||||||
|
help = 'Print configuration and quit',
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user