Compare commits
39 Commits
openapi
...
init-batch
Author | SHA1 | Date | |
---|---|---|---|
06cfbc1692
|
|||
b184688db0
|
|||
2a20d66776
|
|||
4793df09c0
|
|||
5bb294a54b
|
|||
9665ab6d17
|
|||
5bbf2ae940
|
|||
622099cc46
|
|||
8a523dd850
|
|||
75dd1d9f51
|
|||
b03fd794aa
|
|||
19e5b18855
|
|||
0f5f808761
|
|||
46f6de5d61
|
|||
a7ff594548
|
|||
eef3a6bc07
|
|||
d3bb47036c
|
|||
22481da4fa
|
|||
b13364c9b3
|
|||
944bf92150
|
|||
3c4f6ccf8c
|
|||
8161e5e92a
|
|||
4e356f122a
|
|||
f62011d6f7
|
|||
2242b3ce74
|
|||
b2432e782e
|
|||
ec448c9f57
|
|||
80fafbe3df
|
|||
fa180ca354
|
|||
b3f80888d5
|
|||
77175cbb3a
|
|||
36ddc59253
|
|||
d999d6710c
|
|||
d678b1f525
|
|||
1e3a24f575
|
|||
832c95198d
|
|||
03f221a807
|
|||
369180ff85
|
|||
1550c1f2e3
|
README.mdalembic.iniconfig-template.tomluv.lock
data
flake.lockflake.nixpoetry.lockpyproject.tomlsrc
worblehat-frontend
worblehat
__init__.py
cli
deadline_daemon
devscripts
flaskapp
main.pymodels
Author.pyBase.pyBookcase.pyBookcaseItem.pyBookcaseItemBorrowing.pyBookcaseItemBorrowingQueue.pyBookcaseShelf.pyCategory.pyDeadlineDaemonLastRunDatetime.pyLanguage.pyMediaType.py__init__.py
migrations
mixins
xref_tables
services
worblehat
51
README.md
51
README.md
@ -24,12 +24,13 @@ Worblehatt har vært påbegynnt flere ganger opp gjennom historien uten å komme
|
||||
|
||||
## Setup
|
||||
|
||||
This project uses [poetry][poetry] as its buildtool as of May 2023.
|
||||
This project uses `uv` as its buildtool as of February 2025.
|
||||
|
||||
```console
|
||||
$ poetry install
|
||||
$ poetry run alembic migrate
|
||||
$ poetry run worblehat --help
|
||||
$ uv run alembic -x config=./config-template.toml upgrade head
|
||||
$ uv run worblehat -c config-template.toml devscripts seed-test-data
|
||||
$ uv run worblehat --help
|
||||
$ uv run worblehat -c config-template.toml cli
|
||||
```
|
||||
|
||||
## How to configure
|
||||
@ -42,44 +43,4 @@ Unless provided through the `--config` flag, program will automatically look for
|
||||
- `~/.config/worblehat/config.toml`
|
||||
- `/var/lib/worblehat/config.toml`
|
||||
|
||||
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
|
||||
Run `uv run worblehat --help` for more info
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = worblehat/models/migrations
|
||||
script_location = src/worblehat/models/migrations
|
||||
|
||||
# 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
|
||||
|
@ -33,5 +33,8 @@ from = 'worblehat@pvv.ntnu.no'
|
||||
subject_prefix = '[Worblehat]'
|
||||
|
||||
[deadline_daemon]
|
||||
warn_days_before_borrow_deadline = [ "5", "1" ]
|
||||
warn_days_before_expiring_queue_position_deadline = [ "3", "1" ]
|
||||
enabled = true
|
||||
dryrun = false
|
||||
warn_days_before_borrowing_deadline = [ 5, 1 ]
|
||||
days_before_queue_position_expires = 14
|
||||
warn_days_before_expiring_queue_position_deadline = [ 3, 1 ]
|
@ -1,33 +1,29 @@
|
||||
isbn, note, bookcase, shelf
|
||||
9780486809038, emily riehl, arbeidsrom_smal, 5
|
||||
9781568811307, winning ways, arbeidsrom_smal, 5
|
||||
9780486458731, cardano, arbeidsrom_smal, 5
|
||||
9780486462394, alg topo, arbeidsrom_smal, 5
|
||||
9780582447585, formulae, arbeidsrom_smal, 5
|
||||
9780486466668, theory of numbers, arbeidsrom_smal, 5
|
||||
9780486462431, conv surf, arbeidsrom_smal, 5
|
||||
9780486449685, math fun and earnest, arbeidsrom_smal, 5
|
||||
9780486417103, lin prog, arbeidsrom_smal, 5
|
||||
9780130892393, complex analysis, arbeidsrom_smal, 5
|
||||
9781292024967, abstract alg, arbeidsrom_smal, 5
|
||||
9780471728979, kreyzig, arbeidsrom_smal, 5
|
||||
9781847762399, calc 1, arbeidsrom_smal, 5
|
||||
9781847762399, calc 1 again, arbeidsrom_smal, 5
|
||||
9781787267763, calc 1 nome, arbeidsrom_smal, 5
|
||||
9781787267770, calc 2 nome, arbeidsrom_smal, 5
|
||||
9780199208258, non lin ode, arbeidsrom_smal, 5
|
||||
9788251915953, tabeller, arbeidsrom_smal, 5
|
||||
9788251915953, taeller 2, arbeidsrom_smal, 5
|
||||
9788251915953, tabeller 3, arbeidsrom_smal, 5
|
||||
9788251915953, tabeller 4, arbeidsrom_smal, 5
|
||||
9780750304009, fractals and chaos, arbeidsrom_smal, 5
|
||||
9788241902116, geometri, arbeidsrom_smal, 5
|
||||
9781620402788, simpsons, arbeidsrom_smal, 5
|
||||
9781846683459, math curiosities, arbeidsrom_smal, 5
|
||||
9789810245344, fuzzy logic, arbeidsrom_smal, 5
|
||||
9781429224048, vect calc, 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
|
||||
isbn,note,bookcase,shelf
|
||||
9780486809038,emily riehl,arbeidsrom_smal,5
|
||||
9781568811307,winning ways,arbeidsrom_smal,5
|
||||
9780486458731,cardano,arbeidsrom_smal,5
|
||||
9780486462394,alg topo,arbeidsrom_smal,5
|
||||
9780582447585,formulae,arbeidsrom_smal,5
|
||||
9780486466668,theory of numbers,arbeidsrom_smal,5
|
||||
9780486462431,conv surf,arbeidsrom_smal,5
|
||||
9780486449685,math fun and earnest,arbeidsrom_smal,5
|
||||
9780486417103,lin prog,arbeidsrom_smal,5
|
||||
9780130892393,complex analysis,arbeidsrom_smal,5
|
||||
9781292024967,abstract alg,arbeidsrom_smal,5
|
||||
9780471728979,kreyzig,arbeidsrom_smal,5
|
||||
9781847762399,calc 1,arbeidsrom_smal,5
|
||||
9781787267763,calc 1 nome,arbeidsrom_smal,5
|
||||
9781787267770,calc 2 nome,arbeidsrom_smal,5
|
||||
9780199208258,non lin ode,arbeidsrom_smal,5
|
||||
9788251915953,tabeller,arbeidsrom_smal,5
|
||||
9780750304009,fractals and chaos,arbeidsrom_smal,5
|
||||
9788241902116,geometri,arbeidsrom_smal,5
|
||||
9781620402788,simpsons,arbeidsrom_smal,5
|
||||
9781846683459,math curiosities,arbeidsrom_smal,5
|
||||
9789810245344,fuzzy logic,arbeidsrom_smal,5
|
||||
9781429224048,vect calc,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
|
||||
|
|
71
flake.lock
generated
71
flake.lock
generated
@ -1,24 +1,79 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1683014792,
|
||||
"narHash": "sha256-6Va9iVtmmsw4raBc3QKvQT2KT/NGRWlvUlJj46zN8B8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1a411f23ba299db155a5b45d5e145b85a7aafc42",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"id": "flake-utils",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"libdib": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1749301134,
|
||||
"narHash": "sha256-JHLVV4ug8AgG71xhXEdmXozQfesXut6NUdXbBZNkD3c=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "ca26131c22bb2833c81254dbabab6d785b9f37f0",
|
||||
"revCount": 8,
|
||||
"type": "git",
|
||||
"url": "https://git.pvv.ntnu.no/Projects/libdib.git"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://git.pvv.ntnu.no/Projects/libdib.git"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1749213349,
|
||||
"narHash": "sha256-UAaWOyQhdp7nXzsbmLVC67fo+QetzoTm9hsPf9X3yr4=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a4ff0e3c64846abea89662bfbacf037ef4b34207",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"libdib": "libdib",
|
||||
"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",
|
||||
|
108
flake.nix
108
flake.nix
@ -1,43 +1,93 @@
|
||||
{
|
||||
# inputs.nixpkgs.url = "nixpkgs/nixos-22.11";
|
||||
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-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 {
|
||||
apps.${system} = let
|
||||
app = program: {
|
||||
apps = forAllSystems (system: pkgs: let
|
||||
mkApp = package: {
|
||||
type = "app";
|
||||
inherit program;
|
||||
program = lib.getExe package;
|
||||
};
|
||||
in {
|
||||
default = self.apps.${system}.worblehat;
|
||||
worblehat = app "${self.packages.${system}.worblehat}/bin/worblehat";
|
||||
};
|
||||
default = mkApp self.packages.${system}.default;
|
||||
});
|
||||
|
||||
packages.${system} = {
|
||||
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 = forAllSystems (system: pkgs: {
|
||||
default = self.packages.${system}.worblehat;
|
||||
worblehat = with pkgs.python3Packages; buildPythonPackage {
|
||||
pname = "worblehat";
|
||||
version = "0.1.0";
|
||||
worblehat = let
|
||||
inherit (pkgs) python3Packages;
|
||||
pyproject = lib.pipe ./pyproject.toml [
|
||||
builtins.readFile
|
||||
builtins.fromTOML
|
||||
];
|
||||
in python3Packages.buildPythonApplication {
|
||||
pname = pyproject.project.name;
|
||||
version = pyproject.project.version;
|
||||
src = ./.;
|
||||
|
||||
format = "pyproject";
|
||||
|
||||
buildInputs = [ poetry-core ];
|
||||
propagatedBuildInputs = [
|
||||
alembic
|
||||
click
|
||||
flask
|
||||
flask-admin
|
||||
flask-sqlalchemy
|
||||
isbnlib
|
||||
python-dotenv
|
||||
sqlalchemy
|
||||
];
|
||||
build-system = with python3Packages; [ hatchling ];
|
||||
dependencies = deps pkgs.python3Packages;
|
||||
|
||||
meta.mainProgram = "worblehat";
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
561
poetry.lock
generated
561
poetry.lock
generated
@ -1,561 +0,0 @@
|
||||
# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.10.4"
|
||||
description = "A database migration tool for SQLAlchemy."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "alembic-1.10.4-py3-none-any.whl", hash = "sha256:43942c3d4bf2620c466b91c0f4fca136fe51ae972394a0cc8b90810d664e4f5c"},
|
||||
{file = "alembic-1.10.4.tar.gz", hash = "sha256:295b54bbb92c4008ab6a7dcd1e227e668416d6f84b98b3c4446a2bc6214a556b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Mako = "*"
|
||||
SQLAlchemy = ">=1.3.0"
|
||||
typing-extensions = ">=4"
|
||||
|
||||
[package.extras]
|
||||
tz = ["python-dateutil"]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.6.2"
|
||||
description = "Fast, simple object-to-object and broadcast signaling"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"},
|
||||
{file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.3"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "2.3.2"
|
||||
description = "A simple framework for building complex web applications."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Flask-2.3.2-py3-none-any.whl", hash = "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0"},
|
||||
{file = "Flask-2.3.2.tar.gz", hash = "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
blinker = ">=1.6.2"
|
||||
click = ">=8.1.3"
|
||||
itsdangerous = ">=2.1.2"
|
||||
Jinja2 = ">=3.1.2"
|
||||
Werkzeug = ">=2.3.3"
|
||||
|
||||
[package.extras]
|
||||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-admin"
|
||||
version = "1.6.1"
|
||||
description = "Simple and extensible admin interface framework for Flask"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "Flask-Admin-1.6.1.tar.gz", hash = "sha256:24cae2af832b6a611a01d7dc35f42d266c1d6c75a426b869d8cb241b78233369"},
|
||||
{file = "Flask_Admin-1.6.1-py3-none-any.whl", hash = "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=0.7"
|
||||
wtforms = "*"
|
||||
|
||||
[package.extras]
|
||||
aws = ["boto"]
|
||||
azure = ["azure-storage-blob"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-sqlalchemy"
|
||||
version = "3.0.3"
|
||||
description = "Add SQLAlchemy support to your Flask application."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Flask-SQLAlchemy-3.0.3.tar.gz", hash = "sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec"},
|
||||
{file = "Flask_SQLAlchemy-3.0.3-py3-none-any.whl", hash = "sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=2.2"
|
||||
SQLAlchemy = ">=1.4.18"
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "2.0.2"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
files = [
|
||||
{file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"},
|
||||
{file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"},
|
||||
{file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"},
|
||||
{file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"},
|
||||
{file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"},
|
||||
{file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"},
|
||||
{file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx", "docutils (<0.18)"]
|
||||
test = ["objgraph", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "isbnlib"
|
||||
version = "3.10.14"
|
||||
description = "Extract, clean, transform, hyphenate and metadata for ISBNs (International Standard Book Number)."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "isbnlib-3.10.14-py2.py3-none-any.whl", hash = "sha256:f885b350fc8e600a919ed46e3b07253062cd604af69885455a25a299217b3fe2"},
|
||||
{file = "isbnlib-3.10.14.tar.gz", hash = "sha256:96f90864c77b01f55fa11e5bfca9fd909501d9842f3bc710d4eab85195d90539"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.1.2"
|
||||
description = "Safely pass data to untrusted environments and back."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
|
||||
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.2.4"
|
||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"},
|
||||
{file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.9.2"
|
||||
|
||||
[package.extras]
|
||||
babel = ["Babel"]
|
||||
lingua = ["lingua"]
|
||||
testing = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.2"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
|
||||
{file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
|
||||
{file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
|
||||
{file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
|
||||
{file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
|
||||
{file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
|
||||
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pastel"
|
||||
version = "0.2.1"
|
||||
description = "Bring colors to your terminal."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
files = [
|
||||
{file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},
|
||||
{file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poethepoet"
|
||||
version = "0.20.0"
|
||||
description = "A task runner that works well with poetry."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "poethepoet-0.20.0-py3-none-any.whl", hash = "sha256:cb37be15f3895ccc65ddf188c2e3d8fb79e26cc9d469a6098cb1c6f994659f6f"},
|
||||
{file = "poethepoet-0.20.0.tar.gz", hash = "sha256:ca5a2a955f52dfb0a53fad3c989ef0b69ce3d5ec0f6bfa9b1da1f9e32d262e20"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pastel = ">=0.2.1,<0.3.0"
|
||||
tomli = ">=1.2.2"
|
||||
|
||||
[package.extras]
|
||||
poetry-plugin = ["poetry (>=1.0,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.6"
|
||||
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "psycopg2-binary-2.9.6.tar.gz", hash = "sha256:1f64dcfb8f6e0c014c7f55e51c9759f024f70ea572fbdef123f85318c297947c"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d26e0342183c762de3276cca7a530d574d4e25121ca7d6e4a98e4f05cb8e4df7"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c48d8f2db17f27d41fb0e2ecd703ea41984ee19362cbce52c097963b3a1b4365"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe9dc0a884a8848075e576c1de0290d85a533a9f6e9c4e564f19adf8f6e54a7"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a76e027f87753f9bd1ab5f7c9cb8c7628d1077ef927f5e2446477153a602f2c"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6460c7a99fc939b849431f1e73e013d54aa54293f30f1109019c56a0b2b2ec2f"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae102a98c547ee2288637af07393dd33f440c25e5cd79556b04e3fca13325e5f"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9972aad21f965599ed0106f65334230ce826e5ae69fda7cbd688d24fa922415e"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a40c00dbe17c0af5bdd55aafd6ff6679f94a9be9513a4c7e071baf3d7d22a70"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:cacbdc5839bdff804dfebc058fe25684cae322987f7a38b0168bc1b2df703fb1"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7f0438fa20fb6c7e202863e0d5ab02c246d35efb1d164e052f2f3bfe2b152bd0"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-win32.whl", hash = "sha256:b6c8288bb8a84b47e07013bb4850f50538aa913d487579e1921724631d02ea1b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp310-cp310-win_amd64.whl", hash = "sha256:61b047a0537bbc3afae10f134dc6393823882eb263088c271331602b672e52e9"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:964b4dfb7c1c1965ac4c1978b0f755cc4bd698e8aa2b7667c575fb5f04ebe06b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afe64e9b8ea66866a771996f6ff14447e8082ea26e675a295ad3bdbffdd72afb"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e2ee79e7cf29582ef770de7dab3d286431b01c3bb598f8e05e09601b890081"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa74c903a3c1f0d9b1c7e7b53ed2d929a4910e272add6700c38f365a6002820"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b83456c2d4979e08ff56180a76429263ea254c3f6552cd14ada95cff1dec9bb8"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0645376d399bfd64da57148694d78e1f431b1e1ee1054872a5713125681cf1be"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99e34c82309dd78959ba3c1590975b5d3c862d6f279f843d47d26ff89d7d7e1"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4ea29fc3ad9d91162c52b578f211ff1c931d8a38e1f58e684c45aa470adf19e2"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4ac30da8b4f57187dbf449294d23b808f8f53cad6b1fc3623fa8a6c11d176dd0"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e78e6e2a00c223e164c417628572a90093c031ed724492c763721c2e0bc2a8df"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-win32.whl", hash = "sha256:1876843d8e31c89c399e31b97d4b9725a3575bb9c2af92038464231ec40f9edb"},
|
||||
{file = "psycopg2_binary-2.9.6-cp311-cp311-win_amd64.whl", hash = "sha256:b4b24f75d16a89cc6b4cdff0eb6a910a966ecd476d1e73f7ce5985ff1328e9a6"},
|
||||
{file = "psycopg2_binary-2.9.6-cp36-cp36m-win32.whl", hash = "sha256:498807b927ca2510baea1b05cc91d7da4718a0f53cb766c154c417a39f1820a0"},
|
||||
{file = "psycopg2_binary-2.9.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0d236c2825fa656a2d98bbb0e52370a2e852e5a0ec45fc4f402977313329174d"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:34b9ccdf210cbbb1303c7c4db2905fa0319391bd5904d32689e6dd5c963d2ea8"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d2222e61f313c4848ff05353653bf5f5cf6ce34df540e4274516880d9c3763"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30637a20623e2a2eacc420059be11527f4458ef54352d870b8181a4c3020ae6b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8122cfc7cae0da9a3077216528b8bb3629c43b25053284cc868744bfe71eb141"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38601cbbfe600362c43714482f43b7c110b20cb0f8172422c616b09b85a750c5"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c7e62ab8b332147a7593a385d4f368874d5fe4ad4e341770d4983442d89603e3"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2ab652e729ff4ad76d400df2624d223d6e265ef81bb8aa17fbd63607878ecbee"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c83a74b68270028dc8ee74d38ecfaf9c90eed23c8959fca95bd703d25b82c88e"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d4e6036decf4b72d6425d5b29bbd3e8f0ff1059cda7ac7b96d6ac5ed34ffbacd"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-win32.whl", hash = "sha256:a8c28fd40a4226b4a84bdf2d2b5b37d2c7bd49486b5adcc200e8c7ec991dfa7e"},
|
||||
{file = "psycopg2_binary-2.9.6-cp37-cp37m-win_amd64.whl", hash = "sha256:51537e3d299be0db9137b321dfb6a5022caaab275775680e0c3d281feefaca6b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4499e0a83b7b7edcb8dabecbd8501d0d3a5ef66457200f77bde3d210d5debb"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e13a5a2c01151f1208d5207e42f33ba86d561b7a89fca67c700b9486a06d0e2"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e0f754d27fddcfd74006455b6e04e6705d6c31a612ec69ddc040a5468e44b4e"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d57c3fd55d9058645d26ae37d76e61156a27722097229d32a9e73ed54819982a"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71f14375d6f73b62800530b581aed3ada394039877818b2d5f7fc77e3bb6894d"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:441cc2f8869a4f0f4bb408475e5ae0ee1f3b55b33f350406150277f7f35384fc"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:65bee1e49fa6f9cf327ce0e01c4c10f39165ee76d35c846ade7cb0ec6683e303"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:af335bac6b666cc6aea16f11d486c3b794029d9df029967f9938a4bed59b6a19"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cfec476887aa231b8548ece2e06d28edc87c1397ebd83922299af2e051cf2827"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65c07febd1936d63bfde78948b76cd4c2a411572a44ac50719ead41947d0f26b"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-win32.whl", hash = "sha256:4dfb4be774c4436a4526d0c554af0cc2e02082c38303852a36f6456ece7b3503"},
|
||||
{file = "psycopg2_binary-2.9.6-cp38-cp38-win_amd64.whl", hash = "sha256:02c6e3cf3439e213e4ee930308dc122d6fb4d4bea9aef4a12535fbd605d1a2fe"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9182eb20f41417ea1dd8e8f7888c4d7c6e805f8a7c98c1081778a3da2bee3e4"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a6979cf527e2603d349a91060f428bcb135aea2be3201dff794813256c274f1"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8338a271cb71d8da40b023a35d9c1e919eba6cbd8fa20a54b748a332c355d896"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3ed340d2b858d6e6fb5083f87c09996506af483227735de6964a6100b4e6a54"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f81e65376e52f03422e1fb475c9514185669943798ed019ac50410fb4c4df232"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb13af3c5dd3a9588000910178de17010ebcccd37b4f9794b00595e3a8ddad3"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4c727b597c6444a16e9119386b59388f8a424223302d0c06c676ec8b4bc1f963"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4d67fbdaf177da06374473ef6f7ed8cc0a9dc640b01abfe9e8a2ccb1b1402c1f"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0892ef645c2fabb0c75ec32d79f4252542d0caec1d5d949630e7d242ca4681a3"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:02c0f3757a4300cf379eb49f543fb7ac527fb00144d39246ee40e1df684ab514"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-win32.whl", hash = "sha256:c3dba7dab16709a33a847e5cd756767271697041fbe3fe97c215b1fc1f5c9848"},
|
||||
{file = "psycopg2_binary-2.9.6-cp39-cp39-win_amd64.whl", hash = "sha256:f6a88f384335bb27812293fdb11ac6aee2ca3f51d3c7820fe03de0a304ab6249"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.12"
|
||||
description = "Database Abstraction Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "SQLAlchemy-2.0.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10f1ff0ebe21d2cea89ead231ba3ecf75678463ab85f19ce2ce91207620737f3"},
|
||||
{file = "SQLAlchemy-2.0.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:978bee4ecbcdadf087220618409fb9be9509458df479528b70308f0599c7c519"},
|
||||
{file = "SQLAlchemy-2.0.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53b2c8adbcbb59732fb21a024aaa261983655845d86e3fc26a5676cec0ebaa09"},
|
||||
{file = "SQLAlchemy-2.0.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91f4b1bdc987ef85fe3a0ce5d26ac72ff8f60207b08272aa2a65494836391d69"},
|
||||
{file = "SQLAlchemy-2.0.12-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dfd6385b662aea83e63dd4db5fe116eb11914022deb1745f0b57fa8470c18ffe"},
|
||||
{file = "SQLAlchemy-2.0.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5e9d390727c11b9a7e583bf6770de36895c0936bddb98ae93ae99282e6428d5f"},
|
||||
{file = "SQLAlchemy-2.0.12-cp310-cp310-win32.whl", hash = "sha256:a4709457f1c317e347051498b91fa2b86c4bcdebf93c84e6d121a4fc8a397307"},
|
||||
{file = "SQLAlchemy-2.0.12-cp310-cp310-win_amd64.whl", hash = "sha256:f0843132168b44ca33c5e5a2046c954775dde8c580ce27f5cf2e134d0d9919e4"},
|
||||
{file = "SQLAlchemy-2.0.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32762dba51b663609757f861584a722093487f53737e76474cc6e190904dc31b"},
|
||||
{file = "SQLAlchemy-2.0.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d709f43caee115b03b707b8cbbcb8b303045dd7cdc825b6d29857d71f3425ae"},
|
||||
{file = "SQLAlchemy-2.0.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fe98e9d26778d7711ceee2c671741b4f54c74677668481d733d6f70747d7690"},
|
||||
{file = "SQLAlchemy-2.0.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a3101252f3de9a18561c1fb0a68b1ee465485990aba458d4510f214bd5a582c"},
|
||||
{file = "SQLAlchemy-2.0.12-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b1fa0ffc378a7061c452cb4a1f804fad1b3b8aa8d0552725531d27941b2e3ed"},
|
||||
{file = "SQLAlchemy-2.0.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c5268ec05c21e2ecf5bca09314bcaadfec01f02163088cd602db4379862958dd"},
|
||||
{file = "SQLAlchemy-2.0.12-cp311-cp311-win32.whl", hash = "sha256:77a06b0983faf9aa48ee6219d41ade39dee16ce90857cc181dbcf6918acd234d"},
|
||||
{file = "SQLAlchemy-2.0.12-cp311-cp311-win_amd64.whl", hash = "sha256:a022c588c0f413f8cddf9fcc597dbf317efeac4186d8bff9aa7f3219258348b0"},
|
||||
{file = "SQLAlchemy-2.0.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6ceca432ce88ad12aab5b5896c343a1993c90b325d9193dcd055e73e18a0439"},
|
||||
{file = "SQLAlchemy-2.0.12-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e5501c78b5ab917f0f0f75ce7f0018f683a0a76e95f30e6561bf61c9ff69d43"},
|
||||
{file = "SQLAlchemy-2.0.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67efd00ce7f428a446ce012673c03c63c5abb5dec3f33750087b8bdc173bf0"},
|
||||
{file = "SQLAlchemy-2.0.12-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1fac17c866111283cbcdb7024d646abb71fdd95f3ce975cf3710258bc55742fd"},
|
||||
{file = "SQLAlchemy-2.0.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f30c5608c64fc9c1fa9a16277eb4784f782362566fe40ff8d283358c8f2c5fe0"},
|
||||
{file = "SQLAlchemy-2.0.12-cp37-cp37m-win32.whl", hash = "sha256:85b0efe1c71459ba435a6593f54a0e39334b16ba383e8010fdb9d0127ca51ba8"},
|
||||
{file = "SQLAlchemy-2.0.12-cp37-cp37m-win_amd64.whl", hash = "sha256:b76c2fde827522e21922418325c1b95c2d795cdecfb4bc261e4d37965199ee7f"},
|
||||
{file = "SQLAlchemy-2.0.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aec5fb36b53125554ecc2285526eb5cc31b21f6cb059993c1c5ca831959de052"},
|
||||
{file = "SQLAlchemy-2.0.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4ad525b9dd17b478a2ed8580d7f2bc46b0f5889153c6b1c099729583e395b4b9"},
|
||||
{file = "SQLAlchemy-2.0.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9796d5c13b2b7f05084d0ce52528cf919f9bde9e0f10672a6393a4490415695"},
|
||||
{file = "SQLAlchemy-2.0.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e1d50592cb24d1947c374c666add65ded7c181ec98a89ed17abbe9b8b2e2ff4"},
|
||||
{file = "SQLAlchemy-2.0.12-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bf83700faa9642388fbd3167db3f6cbb2e88cc8367b8c22204f3f408ee782d25"},
|
||||
{file = "SQLAlchemy-2.0.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:297b752d4f30350b64175bbbd57dc94c061a35f5d1dba088d0a367dbbebabc94"},
|
||||
{file = "SQLAlchemy-2.0.12-cp38-cp38-win32.whl", hash = "sha256:369f6564e68a9c60f0b9dde121def491e651a4ba8dcdd652a93f1cd5977cd85c"},
|
||||
{file = "SQLAlchemy-2.0.12-cp38-cp38-win_amd64.whl", hash = "sha256:7eb25b981cbc9e7df9f56ad7ec4c6d77323090ca4b7147fcdc09d66535377759"},
|
||||
{file = "SQLAlchemy-2.0.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6ebadefc4331dda83c22519e1ea1e61104df6eb38abbb80ab91b0a8527a5c19"},
|
||||
{file = "SQLAlchemy-2.0.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3745dee26a7ee012598577ad3b8f6e6cd50a49b2afa0cde9db668da6bf2c2319"},
|
||||
{file = "SQLAlchemy-2.0.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09205893a84b6bedae0453d3f384f5d2a6499b6e45ad977549894cdcd85d8f1c"},
|
||||
{file = "SQLAlchemy-2.0.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aad66215a3817a7a1d535769773333250de2653c89b53f7e2d42b677d398027"},
|
||||
{file = "SQLAlchemy-2.0.12-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e495ad05a13171fbb5d72fe5993469c8bceac42bcf6b8f9f117a518ee7fbc353"},
|
||||
{file = "SQLAlchemy-2.0.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:03206576ca53f55b9de6e890273e498f4b2e6e687a9db9859bdcd21df5a63e53"},
|
||||
{file = "SQLAlchemy-2.0.12-cp39-cp39-win32.whl", hash = "sha256:87b2c2d13c3d1384859b60eabb3139e169ce68ada1d2963dbd0c7af797f16efe"},
|
||||
{file = "SQLAlchemy-2.0.12-cp39-cp39-win_amd64.whl", hash = "sha256:3c053c3f4c4e45d4c8b27977647566c140d6de3f61a4e2acb92ea24cf9911c7f"},
|
||||
{file = "SQLAlchemy-2.0.12-py3-none-any.whl", hash = "sha256:e752c34f7a2057ebe82c856698b9f277c633d4aad006bddf7af74598567c8931"},
|
||||
{file = "SQLAlchemy-2.0.12.tar.gz", hash = "sha256:bddfc5bd1dee5db0fddc9dab26f800c283f3243e7281bbf107200fed30125f9c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
|
||||
typing-extensions = ">=4.2.0"
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (!=0.4.17)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql-pymssql = ["pymssql"]
|
||||
mssql-pyodbc = ["pyodbc"]
|
||||
mypy = ["mypy (>=0.910)"]
|
||||
mysql = ["mysqlclient (>=1.4.0)"]
|
||||
mysql-connector = ["mysql-connector-python"]
|
||||
oracle = ["cx-oracle (>=7)"]
|
||||
oracle-oracledb = ["oracledb (>=1.0.1)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
|
||||
postgresql-psycopg = ["psycopg (>=3.0.7)"]
|
||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||
pymysql = ["pymysql"]
|
||||
sqlcipher = ["sqlcipher3-binary"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.5.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
|
||||
{file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "2.3.3"
|
||||
description = "The comprehensive WSGI web application library."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Werkzeug-2.3.3-py3-none-any.whl", hash = "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a"},
|
||||
{file = "Werkzeug-2.3.3.tar.gz", hash = "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.1.1"
|
||||
|
||||
[package.extras]
|
||||
watchdog = ["watchdog (>=2.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "wtforms"
|
||||
version = "3.0.1"
|
||||
description = "Form validation and rendering for Python web development."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "WTForms-3.0.1-py3-none-any.whl", hash = "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b"},
|
||||
{file = "WTForms-3.0.1.tar.gz", hash = "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = "*"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "123df985006374b7c4ede4587a2facef89306039e35af84ddc9c516eecd46c89"
|
@ -1,28 +1,37 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "worblehat"
|
||||
version = "0.1.0"
|
||||
description = "Worblehat is a simple library management system written specifically for Programvareverkstedet"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"alembic>=1.13.3",
|
||||
"beautifulsoup4>=4.12.3",
|
||||
"click>=8.1.7",
|
||||
"flask-admin>=1.6.1",
|
||||
"flask-sqlalchemy>=3.1.1",
|
||||
"flask>=3.0.3",
|
||||
"isbnlib>=3.10.14",
|
||||
"libdib",
|
||||
"psycopg2-binary>=2.9.9",
|
||||
"requests>=2.32.3",
|
||||
"sqlalchemy>=2.0.34",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
alembic = "^1.9.4"
|
||||
click = "^8.1.3"
|
||||
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"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"werkzeug",
|
||||
"poethepoet",
|
||||
]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
werkzeug = "^2.3.3"
|
||||
poethepoet = "^0.20.0"
|
||||
[project.scripts]
|
||||
worblehat = "worblehat:main"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
worblehat = "worblehat.main:main"
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.poe.tasks]
|
||||
clean = """
|
||||
@ -39,7 +48,5 @@ downmigrate = "alembic downgrade -1"
|
||||
# delete the migration file with this, there will be no easy way of downgrading
|
||||
cleanmigrations = "git clean -f worblehat/models/migrations/versions"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
[tool.uv.sources]
|
||||
libdib = { git = "https://git.pvv.ntnu.no/Projects/libdib.git" }
|
||||
|
Before Width: 64px | Height: 64px | Size: 3.8 KiB After Width: 64px | Height: 64px | Size: 3.8 KiB |
3
src/worblehat/__init__.py
Normal file
3
src/worblehat/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .main import main
|
||||
|
||||
__all__ = ["main"]
|
3
src/worblehat/cli/__init__.py
Normal file
3
src/worblehat/cli/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .main import WorblehatCli
|
||||
|
||||
__all__ = ["WorblehatCli"]
|
284
src/worblehat/cli/main.py
Normal file
284
src/worblehat/cli/main.py
Normal file
@ -0,0 +1,284 @@
|
||||
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,4 +1,11 @@
|
||||
from .advanced_options import AdvancedOptionsCli
|
||||
from .bookcase_item import BookcaseItemCli
|
||||
from .bookcase_shelf_selector import select_bookcase_shelf
|
||||
from .search import SearchCli
|
||||
from .search import SearchCli
|
||||
|
||||
__all__ = [
|
||||
"AdvancedOptionsCli",
|
||||
"BookcaseItemCli",
|
||||
"select_bookcase_shelf",
|
||||
"SearchCli",
|
||||
]
|
135
src/worblehat/cli/subclis/advanced_options.py
Normal file
135
src/worblehat/cli/subclis/advanced_options.py
Normal file
@ -0,0 +1,135 @@
|
||||
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",
|
||||
},
|
||||
}
|
416
src/worblehat/cli/subclis/bookcase_item.py
Normal file
416
src/worblehat/cli/subclis/bookcase_item.py
Normal file
@ -0,0 +1,416 @@
|
||||
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,22 +1,24 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from worblehat.cli.prompt_utils import InteractiveItemSelector
|
||||
from libdib.repl import InteractiveItemSelector
|
||||
|
||||
from worblehat.models import (
|
||||
Bookcase,
|
||||
BookcaseShelf,
|
||||
)
|
||||
|
||||
|
||||
def select_bookcase_shelf(
|
||||
bookcase: Bookcase,
|
||||
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:
|
||||
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)
|
||||
try:
|
||||
if arg != '' and len(args) > 0:
|
||||
if arg != "" and len(args) > 0:
|
||||
query = query.where(cls.column == int(args[0]))
|
||||
if len(args) > 1:
|
||||
query = query.where(cls.row == int(args[1]))
|
||||
@ -24,22 +26,21 @@ def select_bookcase_shelf(
|
||||
return []
|
||||
|
||||
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)
|
||||
bookcase_shelf_selector = InteractiveItemSelector(
|
||||
cls = BookcaseShelf,
|
||||
sql_session = sql_session,
|
||||
execute_selection = lambda session, cls, arg: session.scalars(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.bookcase == bookcase,
|
||||
cls.column == int(arg.split('-')[0]),
|
||||
cls.row == int(arg.split('-')[1]),
|
||||
cls=BookcaseShelf,
|
||||
sql_session=sql_session,
|
||||
execute_selection=lambda session, cls, arg: session.scalars(
|
||||
select(cls).where(
|
||||
cls.bookcase == bookcase,
|
||||
cls.column == int(arg.split("-")[0]),
|
||||
cls.row == int(arg.split("-")[1]),
|
||||
)
|
||||
).all(),
|
||||
complete_selection = __complete_bookshelf_selection,
|
||||
complete_selection=__complete_bookshelf_selection,
|
||||
)
|
||||
|
||||
bookcase_shelf_selector.cmdloop()
|
||||
return bookcase_shelf_selector.result
|
||||
return bookcase_shelf_selector.result
|
@ -2,7 +2,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
from worblehat.cli.prompt_utils import (
|
||||
from libdib.repl import (
|
||||
NumberedCmd,
|
||||
NumberedItemSelector,
|
||||
)
|
||||
@ -13,55 +13,53 @@ class SearchCli(NumberedCmd):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__()
|
||||
self.sql_session = sql_session
|
||||
|
||||
self.result = None
|
||||
|
||||
def do_search_all(self, _: str):
|
||||
print('TODO: Implement search all')
|
||||
|
||||
print("TODO: Implement search all")
|
||||
|
||||
def do_search_title(self, _: str):
|
||||
while (input_text := input('Enter title: ')) == '':
|
||||
while (input_text := input("Enter title: ")) == "":
|
||||
pass
|
||||
|
||||
items = self.sql_session.scalars(
|
||||
select(BookcaseItem)
|
||||
.where(BookcaseItem.name.ilike(f'%{input_text}%')),
|
||||
select(BookcaseItem).where(BookcaseItem.name.ilike(f"%{input_text}%")),
|
||||
).all()
|
||||
|
||||
if len(items) == 0:
|
||||
print('No items found.')
|
||||
print("No items found.")
|
||||
return
|
||||
|
||||
selector = NumberedItemSelector(
|
||||
items = items,
|
||||
stringify = lambda item: f"{item.name} ({item.isbn})",
|
||||
items=items,
|
||||
stringify=lambda item: f"{item.name} ({item.isbn})",
|
||||
)
|
||||
selector.cmdloop()
|
||||
if selector.result is not None:
|
||||
self.result = selector.result
|
||||
return True
|
||||
|
||||
|
||||
def do_search_author(self, _: str):
|
||||
while (input_text := input('Enter author name: ')) == '':
|
||||
while (input_text := input("Enter author name: ")) == "":
|
||||
pass
|
||||
|
||||
author = self.sql_session.scalars(
|
||||
select(Author)
|
||||
.where(Author.name.ilike(f'%{input_text}%')),
|
||||
select(Author).where(Author.name.ilike(f"%{input_text}%")),
|
||||
).all()
|
||||
|
||||
if len(author) == 0:
|
||||
print('No authors found.')
|
||||
print("No authors found.")
|
||||
return
|
||||
elif len(author) == 1:
|
||||
selected_author = author[0]
|
||||
print('Found author:')
|
||||
print(f" {selected_author.name} ({sum(item.amount for item in selected_author.books)} items)")
|
||||
print("Found author:")
|
||||
print(
|
||||
f" {selected_author.name} ({sum(item.amount for item in selected_author.items)} items)"
|
||||
)
|
||||
else:
|
||||
selector = NumberedItemSelector(
|
||||
items = author,
|
||||
stringify = lambda author: f"{author.name} ({sum(item.amount for item in author.books)} items)",
|
||||
items=author,
|
||||
stringify=lambda author: f"{author.name} ({sum(item.amount for item in author.items)} items)",
|
||||
)
|
||||
selector.cmdloop()
|
||||
if selector.result is None:
|
||||
@ -69,77 +67,73 @@ class SearchCli(NumberedCmd):
|
||||
selected_author = selector.result
|
||||
|
||||
selector = NumberedItemSelector(
|
||||
items = selected_author.books,
|
||||
stringify = lambda item: f"{item.name} ({item.isbn})",
|
||||
items=list(selected_author.items),
|
||||
stringify=lambda item: f"{item.name} ({item.isbn})",
|
||||
)
|
||||
selector.cmdloop()
|
||||
if selector.result is not None:
|
||||
self.result = selector.result
|
||||
return True
|
||||
|
||||
|
||||
def do_search_owner(self, _: str):
|
||||
while (input_text := input('Enter username: ')) == '':
|
||||
while (input_text := input("Enter username: ")) == "":
|
||||
pass
|
||||
|
||||
users = self.sql_session.scalars(
|
||||
select(BookcaseItem.owner)
|
||||
.where(BookcaseItem.owner.ilike(f'%{input_text}%'))
|
||||
.where(BookcaseItem.owner.ilike(f"%{input_text}%"))
|
||||
.distinct(),
|
||||
).all()
|
||||
|
||||
if len(users) == 0:
|
||||
print('No users found.')
|
||||
print("No users found.")
|
||||
return
|
||||
elif len(users) == 1:
|
||||
selected_user = users[0]
|
||||
print('Found user:')
|
||||
print("Found user:")
|
||||
print(f" {selected_user}")
|
||||
else:
|
||||
selector = NumberedItemSelector(items = users)
|
||||
selector = NumberedItemSelector(items=users)
|
||||
selector.cmdloop()
|
||||
if selector.result is None:
|
||||
return
|
||||
selected_user = selector.result
|
||||
|
||||
items = self.sql_session.scalars(
|
||||
select(BookcaseItem)
|
||||
.where(BookcaseItem.owner == selected_user),
|
||||
select(BookcaseItem).where(BookcaseItem.owner == selected_user),
|
||||
).all()
|
||||
|
||||
selector = NumberedItemSelector(
|
||||
items = items,
|
||||
stringify = lambda item: f"{item.name} ({item.isbn})",
|
||||
items=items,
|
||||
stringify=lambda item: f"{item.name} ({item.isbn})",
|
||||
)
|
||||
selector.cmdloop()
|
||||
if selector.result is not None:
|
||||
self.result = selector.result
|
||||
return True
|
||||
|
||||
|
||||
def do_done(self, _: str):
|
||||
return True
|
||||
|
||||
|
||||
funcs = {
|
||||
1: {
|
||||
'f': do_search_all,
|
||||
'doc': 'Search everything',
|
||||
"f": do_search_all,
|
||||
"doc": "Search everything",
|
||||
},
|
||||
2: {
|
||||
'f': do_search_title,
|
||||
'doc': 'Search by title',
|
||||
"f": do_search_title,
|
||||
"doc": "Search by title",
|
||||
},
|
||||
3: {
|
||||
'f': do_search_author,
|
||||
'doc': 'Search by author',
|
||||
"f": do_search_author,
|
||||
"doc": "Search by author",
|
||||
},
|
||||
4: {
|
||||
'f': do_search_owner,
|
||||
'doc': 'Search by owner',
|
||||
"f": do_search_owner,
|
||||
"doc": "Search by owner",
|
||||
},
|
||||
9: {
|
||||
'f': do_done,
|
||||
'doc': 'Done',
|
||||
"f": do_done,
|
||||
"doc": "Done",
|
||||
},
|
||||
}
|
||||
}
|
3
src/worblehat/deadline_daemon/__init__.py
Normal file
3
src/worblehat/deadline_daemon/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .main import DeadlineDaemon
|
||||
|
||||
__all__ = ["DeadlineDaemon"]
|
320
src/worblehat/deadline_daemon/main.py
Normal file
320
src/worblehat/deadline_daemon/main.py
Normal file
@ -0,0 +1,320 @@
|
||||
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()
|
170
src/worblehat/devscripts/batch_scanner.py
Normal file
170
src/worblehat/devscripts/batch_scanner.py
Normal file
@ -0,0 +1,170 @@
|
||||
from worblehat.cli.subclis import select_bookcase_shelf
|
||||
from sqlalchemy import select, event
|
||||
from sqlalchemy.orm.session import Session
|
||||
from worblehat.models import Bookcase, BookcaseShelf
|
||||
from worblehat.services.metadata_fetchers import fetch_metadata_from_multiple_sources
|
||||
from worblehat.services import is_valid_isbn
|
||||
from pprint import pprint
|
||||
from libdib.repl import (
|
||||
NumberedCmd,
|
||||
prompt_yes_no,
|
||||
)
|
||||
|
||||
class BatchScanner(NumberedCmd):
|
||||
def __init__(self, sql_session: Session):
|
||||
super().__init__()
|
||||
self.sql_session = sql_session
|
||||
self.sql_session_dirty = False
|
||||
self.bookcase_shelf = None
|
||||
self.prompt_header = "[SHELF| NONE]"
|
||||
|
||||
@event.listens_for(self.sql_session, "after_flush")
|
||||
def mark_session_as_dirty(*_):
|
||||
self.sql_session_dirty = True
|
||||
self.prompt_header = "(unsaved changes)"
|
||||
|
||||
@event.listens_for(self.sql_session, "after_commit")
|
||||
@event.listens_for(self.sql_session, "after_rollback")
|
||||
def mark_session_as_clean(*_):
|
||||
self.sql_session_dirty = False
|
||||
self.prompt_header = None
|
||||
|
||||
|
||||
def _set_bookcase_shelf(self, bookcase_shelf: BookcaseShelf):
|
||||
self.bookcase_shelf = bookcase_shelf
|
||||
self.prompt_header = f"[SHELF| {bookcase_shelf.bookcase.uid} {bookcase_shelf.bookcase.description} / {bookcase_shelf.column}-{bookcase_shelf.row}: {bookcase_shelf.description}]"
|
||||
|
||||
|
||||
def default(self, isbn: str):
|
||||
isbn = isbn.strip()
|
||||
if not is_valid_isbn(isbn):
|
||||
super()._default(isbn)
|
||||
return
|
||||
|
||||
if self.bookcase_shelf is None:
|
||||
print("Please set the bookcase shelf first.")
|
||||
return
|
||||
|
||||
print(f"Scanned ISBN: {isbn}")
|
||||
|
||||
data = fetch_metadata_from_multiple_sources(isbn)
|
||||
|
||||
pprint(data)
|
||||
|
||||
|
||||
def do_set_bookcase_shelf(self, _: str):
|
||||
bookcase = self._choose_bookcase(self.sql_session)
|
||||
|
||||
print()
|
||||
|
||||
self._print_bookcase_shelves(
|
||||
sql_session=self.sql_session,
|
||||
bookcase=bookcase,
|
||||
)
|
||||
|
||||
print()
|
||||
|
||||
bookcase_shelf = select_bookcase_shelf(
|
||||
sql_session=self.sql_session,
|
||||
bookcase=bookcase,
|
||||
)
|
||||
|
||||
self._set_bookcase_shelf(bookcase_shelf)
|
||||
|
||||
print(f"Bookcase shelf set to {bookcase_shelf}")
|
||||
|
||||
def _choose_bookcase(
|
||||
self,
|
||||
sql_session: Session,
|
||||
) -> Bookcase:
|
||||
bookcases = sql_session.scalars(
|
||||
select(Bookcase)
|
||||
).all()
|
||||
|
||||
while True:
|
||||
print("Available bookcases:")
|
||||
for bookcase in bookcases:
|
||||
print(f" {bookcase.name} - {bookcase.description}")
|
||||
|
||||
bookcase_name = input("Choose a bookcase> ").strip()
|
||||
|
||||
bookcase = sql_session.scalars(
|
||||
select(Bookcase).where(Bookcase.name == bookcase_name)
|
||||
).one_or_none()
|
||||
|
||||
if not bookcase:
|
||||
print(f"Bookcase {bookcase_name} not found")
|
||||
continue
|
||||
|
||||
return bookcase
|
||||
|
||||
def _print_bookcase_shelves(
|
||||
self,
|
||||
sql_session: Session,
|
||||
bookcase: Bookcase,
|
||||
) -> None:
|
||||
shelves = sql_session.scalars(
|
||||
select(BookcaseShelf).where(BookcaseShelf.bookcase == bookcase)
|
||||
).all()
|
||||
min_col = min([shelf.column for shelf in shelves])
|
||||
max_col = max([shelf.column for shelf in shelves])
|
||||
min_row = min([shelf.row for shelf in shelves])
|
||||
max_row = max([shelf.row for shelf in shelves])
|
||||
|
||||
print('Available shelves:')
|
||||
for col in range(min_col, max_col + 1):
|
||||
for row in range(min_row, max_row + 1):
|
||||
shelf = sql_session.scalars(
|
||||
select(BookcaseShelf).where(
|
||||
BookcaseShelf.bookcase == bookcase,
|
||||
BookcaseShelf.column == col,
|
||||
BookcaseShelf.row == row,
|
||||
)
|
||||
).one_or_none()
|
||||
if shelf:
|
||||
print(f"{col}-{row}: {shelf.description}")
|
||||
else:
|
||||
print(f"{col}-{row}: None")
|
||||
|
||||
def do_save(self, _: str):
|
||||
if not self.sql_session_dirty:
|
||||
print("No changes to save.")
|
||||
return
|
||||
self.sql_session.commit()
|
||||
|
||||
def do_abort(self, _: str):
|
||||
if not self.sql_session_dirty:
|
||||
print("No changes to abort.")
|
||||
return
|
||||
self.sql_session.rollback()
|
||||
|
||||
def do_exit(self, _: str):
|
||||
if self.sql_session_dirty:
|
||||
if prompt_yes_no("Would you like to save your changes?"):
|
||||
self.sql_session.commit()
|
||||
else:
|
||||
self.sql_session.rollback()
|
||||
exit(0)
|
||||
|
||||
funcs = {
|
||||
0: {
|
||||
"f": default,
|
||||
"doc": "Add item with its ISBN",
|
||||
},
|
||||
1: {
|
||||
'f' : do_set_bookcase_shelf,
|
||||
'doc' : 'Select shelf',
|
||||
},
|
||||
7: {
|
||||
"f": do_save,
|
||||
"doc": "Save changes",
|
||||
},
|
||||
8: {
|
||||
"f": do_abort,
|
||||
"doc": "Abort changes",
|
||||
},
|
||||
9: {
|
||||
"f": do_exit,
|
||||
"doc": "Exit",
|
||||
},
|
||||
}
|
127
src/worblehat/devscripts/seed_content_for_deadline_daemon.py
Normal file
127
src/worblehat/devscripts/seed_content_for_deadline_daemon.py
Normal file
@ -0,0 +1,127 @@
|
||||
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()
|
87
src/worblehat/devscripts/seed_test_data.py
Normal file
87
src/worblehat/devscripts/seed_test_data.py
Normal file
@ -0,0 +1,87 @@
|
||||
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()
|
0
src/worblehat/flaskapp/api/bookcase.py
Normal file
0
src/worblehat/flaskapp/api/bookcase.py
Normal file
0
src/worblehat/flaskapp/blueprints/__init__.py
Normal file
0
src/worblehat/flaskapp/blueprints/__init__.py
Normal file
@ -2,10 +2,12 @@ from flask import Blueprint, render_template
|
||||
|
||||
main = Blueprint("main", __name__, template_folder="main")
|
||||
|
||||
@main.route('/')
|
||||
|
||||
@main.route("/")
|
||||
def index():
|
||||
return render_template("main/index.html")
|
||||
|
||||
|
||||
@main.route("/login")
|
||||
def login():
|
||||
return render_template("main/login.html")
|
||||
return render_template("main/login.html")
|
@ -1,3 +1,3 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
db = SQLAlchemy()
|
@ -10,18 +10,19 @@ from worblehat.services.config import Config
|
||||
from .blueprints.main import main
|
||||
from .database import db
|
||||
|
||||
|
||||
def create_app(args: dict[str, any] | None = None):
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config.update(Config['flask'])
|
||||
app.config.update(Config["flask"])
|
||||
app.config.update(Config._config)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = Config.db_string()
|
||||
app.config['SQLALCHEMY_ECHO'] = Config['logging.debug_sql']
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = Config.db_string()
|
||||
app.config["SQLALCHEMY_ECHO"] = Config["logging.debug_sql"]
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
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)
|
||||
seed_data()
|
||||
|
||||
@ -31,12 +32,13 @@ def create_app(args: dict[str, any] | None = None):
|
||||
|
||||
return 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(Bookcase, db.session))
|
||||
admin.add_view(ModelView(BookcaseItem, db.session))
|
||||
admin.add_view(ModelView(BookcaseShelf, db.session))
|
||||
admin.add_view(ModelView(Category, db.session))
|
||||
admin.add_view(ModelView(Language, db.session))
|
||||
admin.add_view(ModelView(MediaType, db.session))
|
||||
admin.add_view(ModelView(MediaType, db.session))
|
19
src/worblehat/flaskapp/wsgi_dev.py
Normal file
19
src/worblehat/flaskapp/wsgi_dev.py
Normal file
@ -0,0 +1,19 @@
|
||||
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,8 +1,10 @@
|
||||
from .flaskapp import create_app
|
||||
|
||||
|
||||
def main():
|
||||
app = create_app()
|
||||
app.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
104
src/worblehat/main.py
Normal file
104
src/worblehat/main.py
Normal file
@ -0,0 +1,104 @@
|
||||
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)
|
||||
elif args.script == "batch-scanner":
|
||||
from .devscripts.batch_scanner import BatchScanner
|
||||
|
||||
result = BatchScanner(sql_session).cmdloop()
|
||||
|
||||
print(result)
|
||||
else:
|
||||
print(devscripts_arg_parser.format_help())
|
||||
exit(1)
|
||||
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,13 +1,8 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
ForeignKey,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship,
|
||||
)
|
||||
|
||||
@ -21,14 +16,15 @@ from .xref_tables import Item_Author
|
||||
if TYPE_CHECKING:
|
||||
from .BookcaseItem import BookcaseItem
|
||||
|
||||
|
||||
class Author(Base, UidMixin, UniqueNameMixin):
|
||||
items: Mapped[set[BookcaseItem]] = relationship(
|
||||
secondary = Item_Author.__table__,
|
||||
back_populates = 'authors',
|
||||
secondary=Item_Author.__table__,
|
||||
back_populates="authors",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
):
|
||||
self.name = name
|
||||
self.name = name
|
@ -9,6 +9,7 @@ from sqlalchemy.orm.collections import (
|
||||
InstrumentedSet,
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
metadata = MetaData(
|
||||
naming_convention={
|
||||
@ -16,7 +17,7 @@ class Base(DeclarativeBase):
|
||||
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||
"ck": "ck_%(table_name)s_`%(constraint_name)s`",
|
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||
"pk": "pk_%(table_name)s"
|
||||
"pk": "pk_%(table_name)s",
|
||||
}
|
||||
)
|
||||
|
||||
@ -26,15 +27,18 @@ class Base(DeclarativeBase):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
columns = ", ".join(
|
||||
f"{k}={repr(v)}" for k, v in self.__dict__.items() if not any([
|
||||
k.startswith("_"),
|
||||
|
||||
# Ensure that we don't try to print out the entire list of
|
||||
# relationships, which could create an infinite loop
|
||||
isinstance(v, Base),
|
||||
isinstance(v, InstrumentedList),
|
||||
isinstance(v, InstrumentedSet),
|
||||
isinstance(v, InstrumentedDict),
|
||||
])
|
||||
f"{k}={repr(v)}"
|
||||
for k, v in self.__dict__.items()
|
||||
if not any(
|
||||
[
|
||||
k.startswith("_"),
|
||||
# Ensure that we don't try to print out the entire list of
|
||||
# relationships, which could create an infinite loop
|
||||
isinstance(v, Base),
|
||||
isinstance(v, InstrumentedList),
|
||||
isinstance(v, InstrumentedSet),
|
||||
isinstance(v, InstrumentedDict),
|
||||
]
|
||||
)
|
||||
)
|
||||
return f"<{self.__class__.__name__}({columns})>"
|
||||
return f"<{self.__class__.__name__}({columns})>"
|
@ -13,13 +13,15 @@ from .mixins import (
|
||||
UidMixin,
|
||||
UniqueNameMixin,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .BookcaseShelf import BookcaseShelf
|
||||
|
||||
|
||||
class Bookcase(Base, UidMixin, UniqueNameMixin):
|
||||
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__(
|
||||
self,
|
||||
@ -32,6 +34,5 @@ class Bookcase(Base, UidMixin, UniqueNameMixin):
|
||||
def short_str(self) -> str:
|
||||
result = self.name
|
||||
if self.description is not None:
|
||||
result += f' [{self.description}]'
|
||||
result += f" [{self.description}]"
|
||||
return result
|
||||
|
@ -2,10 +2,10 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
SmallInteger,
|
||||
String,
|
||||
ForeignKey,
|
||||
ForeignKey,
|
||||
SmallInteger,
|
||||
String,
|
||||
Text,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
@ -16,12 +16,12 @@ from sqlalchemy.orm import (
|
||||
from .Base import Base
|
||||
from .mixins import (
|
||||
UidMixin,
|
||||
UniqueNameMixin,
|
||||
)
|
||||
from .xref_tables import (
|
||||
Item_Category,
|
||||
Item_Author,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .Author import Author
|
||||
from .BookcaseItemBorrowing import BookcaseItemBorrowing
|
||||
@ -31,36 +31,51 @@ if TYPE_CHECKING:
|
||||
from .Language import Language
|
||||
from .MediaType import MediaType
|
||||
|
||||
class BookcaseItem(Base, UidMixin, UniqueNameMixin):
|
||||
from worblehat.flaskapp.database import db
|
||||
|
||||
|
||||
class BookcaseItem(Base, UidMixin):
|
||||
isbn: Mapped[int] = mapped_column(String, unique=True, index=True)
|
||||
owner: Mapped[str] = mapped_column(String, default='PVV')
|
||||
name: Mapped[str] = mapped_column(Text, index=True)
|
||||
owner: Mapped[str] = mapped_column(String, default="PVV")
|
||||
amount: Mapped[int] = mapped_column(SmallInteger, default=1)
|
||||
|
||||
fk_media_type_uid: Mapped[int] = mapped_column(ForeignKey('MediaType.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_media_type_uid: Mapped[int] = mapped_column(ForeignKey("MediaType.uid"))
|
||||
fk_bookcase_shelf_uid: Mapped[int] = mapped_column(ForeignKey("BookcaseShelf.uid"))
|
||||
fk_language_uid: Mapped[int | None] = mapped_column(ForeignKey("Language.uid"))
|
||||
|
||||
media_type: Mapped[MediaType] = relationship(back_populates='items')
|
||||
shelf: Mapped[BookcaseShelf] = relationship(back_populates='items')
|
||||
media_type: Mapped[MediaType] = relationship(back_populates="items")
|
||||
shelf: Mapped[BookcaseShelf] = relationship(back_populates="items")
|
||||
language: Mapped[Language] = relationship()
|
||||
borrowings: Mapped[set[BookcaseItemBorrowing]] = relationship(back_populates='item')
|
||||
borrowing_queue: Mapped[set[BookcaseItemBorrowingQueue]] = relationship(back_populates='item')
|
||||
borrowings: Mapped[set[BookcaseItemBorrowing]] = relationship(back_populates="item")
|
||||
borrowing_queue: Mapped[set[BookcaseItemBorrowingQueue]] = relationship(
|
||||
back_populates="item"
|
||||
)
|
||||
|
||||
categories: Mapped[set[Category]] = relationship(
|
||||
secondary = Item_Category.__table__,
|
||||
back_populates = 'items',
|
||||
secondary=Item_Category.__table__,
|
||||
back_populates="items",
|
||||
)
|
||||
authors: Mapped[set[Author]] = relationship(
|
||||
secondary = Item_Author.__table__,
|
||||
back_populates = 'items',
|
||||
secondary=Item_Author.__table__,
|
||||
back_populates="items",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
isbn: int | None = None,
|
||||
owner: str = 'PVV',
|
||||
owner: str = "PVV",
|
||||
):
|
||||
self.name = name
|
||||
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,7 +3,6 @@ from typing import TYPE_CHECKING
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
ForeignKey,
|
||||
String,
|
||||
DateTime,
|
||||
@ -16,18 +15,24 @@ from sqlalchemy.orm import (
|
||||
|
||||
from .Base import Base
|
||||
from .mixins import UidMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .BookcaseItem import BookcaseItem
|
||||
|
||||
|
||||
class BookcaseItemBorrowing(Base, UidMixin):
|
||||
username: Mapped[str] = mapped_column(String)
|
||||
start_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
||||
end_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now() + timedelta(days=30))
|
||||
end_time: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.now() + timedelta(days=30)
|
||||
)
|
||||
delivered: Mapped[datetime | None] = mapped_column(DateTime, default=None)
|
||||
|
||||
fk_bookcase_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), index=True)
|
||||
fk_bookcase_item_uid: Mapped[int] = mapped_column(
|
||||
ForeignKey("BookcaseItem.uid"), index=True
|
||||
)
|
||||
|
||||
item: Mapped[BookcaseItem] = relationship(back_populates='borrowings')
|
||||
item: Mapped[BookcaseItem] = relationship(back_populates="borrowings")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -37,4 +42,4 @@ class BookcaseItemBorrowing(Base, UidMixin):
|
||||
self.username = username
|
||||
self.item = item
|
||||
self.start_time = datetime.now()
|
||||
self.end_time = datetime.now() + timedelta(days=30)
|
||||
self.end_time = datetime.now() + timedelta(days=30)
|
15
worblehat/models/BookcaseItemBorrowingQueue.py → src/worblehat/models/BookcaseItemBorrowingQueue.py
15
worblehat/models/BookcaseItemBorrowingQueue.py → src/worblehat/models/BookcaseItemBorrowingQueue.py
@ -16,17 +16,24 @@ from sqlalchemy.orm import (
|
||||
|
||||
from .Base import Base
|
||||
from .mixins import UidMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .BookcaseItem import BookcaseItem
|
||||
|
||||
|
||||
class BookcaseItemBorrowingQueue(Base, UidMixin):
|
||||
username: Mapped[str] = mapped_column(String)
|
||||
entered_queue_time = mapped_column(DateTime, default=datetime.now())
|
||||
entered_queue_time: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.now()
|
||||
)
|
||||
item_became_available_time: Mapped[datetime | None] = mapped_column(DateTime)
|
||||
expired = mapped_column(Boolean, default=False)
|
||||
|
||||
fk_bookcase_item_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseItem.uid'), index=True)
|
||||
fk_bookcase_item_uid: Mapped[int] = mapped_column(
|
||||
ForeignKey("BookcaseItem.uid"), index=True
|
||||
)
|
||||
|
||||
item: Mapped[BookcaseItem] = relationship(back_populates='borrowing_queue')
|
||||
item: Mapped[BookcaseItem] = relationship(back_populates="borrowing_queue")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -35,4 +42,4 @@ class BookcaseItemBorrowingQueue(Base, UidMixin):
|
||||
):
|
||||
self.username = username
|
||||
self.item = item
|
||||
self.entered_queue_time = datetime.now()
|
||||
self.entered_queue_time = datetime.now()
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
ForeignKey,
|
||||
SmallInteger,
|
||||
Text,
|
||||
@ -16,6 +15,7 @@ from sqlalchemy.orm import (
|
||||
|
||||
from .Base import Base
|
||||
from .mixins import UidMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .Bookcase import Bookcase
|
||||
from .BookcaseItem import BookcaseItem
|
||||
@ -23,22 +23,23 @@ if TYPE_CHECKING:
|
||||
# NOTE: Booshelfs are 0 indexed for both rows and columns,
|
||||
# where cell 0-0 is placed in the lower right corner.
|
||||
|
||||
|
||||
class BookcaseShelf(Base, UidMixin):
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
'column',
|
||||
'fk_bookcase_uid',
|
||||
'row',
|
||||
"column",
|
||||
"fk_bookcase_uid",
|
||||
"row",
|
||||
),
|
||||
)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
row: 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')
|
||||
items: Mapped[set[BookcaseItem]] = relationship(back_populates='shelf')
|
||||
bookcase: Mapped[Bookcase] = relationship(back_populates="shelfs")
|
||||
items: Mapped[set[BookcaseItem]] = relationship(back_populates="shelf")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -53,7 +54,7 @@ class BookcaseShelf(Base, UidMixin):
|
||||
self.description = description
|
||||
|
||||
def short_str(self) -> str:
|
||||
result = f'{self.column}-{self.row}'
|
||||
result = f"{self.column}-{self.row}"
|
||||
if self.description is not None:
|
||||
result += f' [{self.description}]'
|
||||
return result
|
||||
result += f" [{self.description}]"
|
||||
return result
|
@ -14,15 +14,17 @@ from .mixins import (
|
||||
UniqueNameMixin,
|
||||
)
|
||||
from .xref_tables import Item_Category
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .BookcaseItem import BookcaseItem
|
||||
|
||||
|
||||
class Category(Base, UidMixin, UniqueNameMixin):
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
items: Mapped[set[BookcaseItem]] = relationship(
|
||||
secondary=Item_Category.__table__,
|
||||
back_populates='categories',
|
||||
back_populates="categories",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@ -31,4 +33,4 @@ class Category(Base, UidMixin, UniqueNameMixin):
|
||||
description: str | None = None,
|
||||
):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.description = description
|
@ -12,12 +12,17 @@ from sqlalchemy.orm import (
|
||||
|
||||
from .Base import Base
|
||||
|
||||
|
||||
class DeadlineDaemonLastRunDatetime(Base):
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
'uid = true',
|
||||
name = 'single_row_only',
|
||||
"uid = true",
|
||||
name="single_row_only",
|
||||
),
|
||||
)
|
||||
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,6 +11,7 @@ from sqlalchemy.orm import (
|
||||
from .Base import Base
|
||||
from .mixins import UidMixin, UniqueNameMixin
|
||||
|
||||
|
||||
class Language(Base, UidMixin, UniqueNameMixin):
|
||||
iso639_1_code: Mapped[str] = mapped_column(String(2), unique=True, index=True)
|
||||
|
@ -10,13 +10,15 @@ from sqlalchemy.orm import (
|
||||
|
||||
from .Base import Base
|
||||
from .mixins import UidMixin, UniqueNameMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .BookcaseItem import BookcaseItem
|
||||
|
||||
|
||||
class MediaType(Base, UidMixin, UniqueNameMixin):
|
||||
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__(
|
||||
self,
|
||||
@ -25,5 +27,3 @@ class MediaType(Base, UidMixin, UniqueNameMixin):
|
||||
):
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
|
@ -8,4 +8,18 @@ from .BookcaseShelf import BookcaseShelf
|
||||
from .Category import Category
|
||||
from .DeadlineDaemonLastRunDatetime import DeadlineDaemonLastRunDatetime
|
||||
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,5 +1,4 @@
|
||||
from alembic import context
|
||||
from flask import current_app
|
||||
from logging.config import fileConfig
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
@ -12,9 +11,14 @@ config = context.config
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
Config.load_configuration({})
|
||||
config_attrs = {}
|
||||
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
|
||||
# https://stackoverflow.com/questions/70203927/how-to-prevent-alembic-revision-autogenerate-from-making-revision-file-if-it-h
|
||||
@ -23,7 +27,8 @@ def _process_revision_directives(context, revision, directives):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
print('No changes in schema detected. Not generating migration.')
|
||||
print("No changes in schema detected. Not generating migration.")
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
connectable = engine_from_config(
|
||||
@ -36,11 +41,9 @@ def run_migrations_online() -> None:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=Base.metadata,
|
||||
|
||||
# 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
|
||||
compare_type=True,
|
||||
|
||||
# This is required for ALTER TABLE to work with sqlite.
|
||||
# It should have no effect on postgreSQL
|
||||
# https://alembic.sqlalchemy.org/en/latest/batch.html
|
||||
@ -51,6 +54,7 @@ def run_migrations_online() -> None:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
# We don't have any good reasons to generate raw sql migrations,
|
||||
# so the `run_migrations_offline` has been removed
|
||||
run_migrations_online()
|
261
src/worblehat/models/migrations/versions/2024-07-31T2107_7dfbf8a8dec8_initial_migration.py
Normal file
261
src/worblehat/models/migrations/versions/2024-07-31T2107_7dfbf8a8dec8_initial_migration.py
Normal file
@ -0,0 +1,261 @@
|
||||
"""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 ###
|
@ -9,6 +9,7 @@ from sqlalchemy.orm import (
|
||||
|
||||
from worblehat.flaskapp.database import db
|
||||
|
||||
|
||||
class UidMixin(object):
|
||||
uid: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
@ -28,4 +29,4 @@ class UidMixin(object):
|
||||
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.uid == uid).one_or_404()
|
||||
return sql_session.query(cls).where(cls.uid == uid).one_or_404()
|
@ -9,6 +9,7 @@ from sqlalchemy.orm import (
|
||||
|
||||
from worblehat.flaskapp.database import db
|
||||
|
||||
|
||||
class UniqueNameMixin(object):
|
||||
name: Mapped[str] = mapped_column(Text, unique=True, index=True)
|
||||
|
||||
@ -28,4 +29,4 @@ class UniqueNameMixin(object):
|
||||
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.name == name).one_or_404()
|
||||
return sql_session.query(cls).where(cls.name == name).one_or_404()
|
@ -1,6 +1,7 @@
|
||||
from sqlalchemy.orm import declared_attr
|
||||
|
||||
|
||||
class XrefMixin(object):
|
||||
@declared_attr.directive
|
||||
def __tablename__(cls) -> str:
|
||||
return f'xref_{cls.__name__.lower()}'
|
||||
return f"xref_{cls.__name__.lower()}"
|
@ -1,2 +1,4 @@
|
||||
from .UidMixin import UidMixin
|
||||
from .UniqueNameMixin import UniqueNameMixin
|
||||
|
||||
__all__ = ["UidMixin", "UniqueNameMixin"]
|
19
src/worblehat/models/xref_tables/Item_Author.py
Normal file
19
src/worblehat/models/xref_tables/Item_Author.py
Normal file
@ -0,0 +1,19 @@
|
||||
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
|
||||
)
|
19
src/worblehat/models/xref_tables/Item_Category.py
Normal file
19
src/worblehat/models/xref_tables/Item_Category.py
Normal file
@ -0,0 +1,19 @@
|
||||
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
|
||||
)
|
7
src/worblehat/models/xref_tables/__init__.py
Normal file
7
src/worblehat/models/xref_tables/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
from .Item_Author import Item_Author
|
||||
from .Item_Category import Item_Category
|
||||
|
||||
__all__ = [
|
||||
"Item_Author",
|
||||
"Item_Category",
|
||||
]
|
21
src/worblehat/services/__init__.py
Normal file
21
src/worblehat/services/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
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",
|
||||
]
|
74
src/worblehat/services/argument_parser.py
Normal file
74
src/worblehat/services/argument_parser.py
Normal file
@ -0,0 +1,74 @@
|
||||
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",
|
||||
)
|
||||
|
||||
devscripts_subparsers.add_parser(
|
||||
"batch-scanner",
|
||||
help="REPL optimized for scanning in tons of books",
|
||||
)
|
||||
|
||||
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",
|
||||
)
|
65
src/worblehat/services/bookcase_item.py
Normal file
65
src/worblehat/services/bookcase_item.py
Normal file
@ -0,0 +1,65 @@
|
||||
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
|
@ -9,7 +9,7 @@ class Config:
|
||||
This class is a singleton which holds the configuration for the
|
||||
application. It is initialized by calling `Config.load_configuration()`
|
||||
with a dictionary of arguments. The arguments are usually the result
|
||||
of calling `vars(arg_parser.parse_args())` where `arg_parser` i s the
|
||||
of calling `vars(arg_parser.parse_args())` where `arg_parser` is the
|
||||
argument parser from `worblehat/services/argument_parser.py`.
|
||||
|
||||
The class also provides some utility functions for accessing several
|
||||
@ -18,27 +18,31 @@ class Config:
|
||||
|
||||
_config = None
|
||||
_expected_config_file_locations = [
|
||||
Path('./config.toml'),
|
||||
Path('~/.config/worblehat/config.toml'),
|
||||
Path('/var/lib/worblehat/config.toml'),
|
||||
Path("./config.toml"),
|
||||
Path("~/.config/worblehat/config.toml"),
|
||||
Path("/var/lib/worblehat/config.toml"),
|
||||
]
|
||||
|
||||
def __class_getitem__(cls, name: str) -> Any:
|
||||
if cls._config is None:
|
||||
raise RuntimeError(
|
||||
"Configuration not loaded, call Config.load_configuration() first."
|
||||
)
|
||||
|
||||
__config = cls._config
|
||||
for attr in name.split('.'):
|
||||
for attr in name.split("."):
|
||||
__config = __config.get(attr)
|
||||
if __config is None:
|
||||
raise AttributeError(f'No such attribute: {name}')
|
||||
raise AttributeError(f"No such attribute: {name}")
|
||||
return __config
|
||||
|
||||
@staticmethod
|
||||
def read_password(password_field: str) -> str:
|
||||
if Path(password_field).is_file():
|
||||
with open(password_field, 'r') as f:
|
||||
return f.read()
|
||||
else:
|
||||
return password_field
|
||||
|
||||
if Path(password_field).is_file():
|
||||
with open(password_field, "r") as f:
|
||||
return f.read()
|
||||
else:
|
||||
return password_field
|
||||
|
||||
@classmethod
|
||||
def _locate_configuration_file(cls) -> Path | None:
|
||||
@ -46,48 +50,46 @@ class Config:
|
||||
if path.is_file():
|
||||
return path
|
||||
|
||||
|
||||
@classmethod
|
||||
def _load_configuration_from_file(cls, config_file_path: str | None) -> dict[str, any]:
|
||||
def _load_configuration_from_file(
|
||||
cls, config_file_path: str | None
|
||||
) -> dict[str, any]:
|
||||
if config_file_path is None:
|
||||
config_file_path = cls._locate_configuration_file()
|
||||
|
||||
if config_file_path is None:
|
||||
print('Error: could not locate configuration file.')
|
||||
print("Error: could not locate configuration file.")
|
||||
exit(1)
|
||||
|
||||
with open(config_file_path, 'rb') as config_file:
|
||||
with open(config_file_path, "rb") as config_file:
|
||||
args = tomllib.load(config_file)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
@classmethod
|
||||
def db_string(cls) -> str:
|
||||
db_type = cls._config.get('database').get('type')
|
||||
db_type = cls._config.get("database").get("type")
|
||||
|
||||
if db_type == 'sqlite':
|
||||
path = Path(cls._config.get('database').get('sqlite').get('path'))
|
||||
if db_type == "sqlite":
|
||||
path = Path(cls._config.get("database").get("sqlite").get("path"))
|
||||
return f"sqlite:///{path.absolute()}"
|
||||
|
||||
elif db_type == 'postgresql':
|
||||
db_config = cls._config.get('database').get('postgresql')
|
||||
hostname = db_config.get('hostname')
|
||||
port = db_config.get('port')
|
||||
username = db_config.get('username')
|
||||
password = cls.read_password(db_config.get('password'))
|
||||
database = db_config.get('database')
|
||||
elif db_type == "postgresql":
|
||||
db_config = cls._config.get("database").get("postgresql")
|
||||
hostname = db_config.get("hostname")
|
||||
port = db_config.get("port")
|
||||
username = db_config.get("username")
|
||||
password = cls.read_password(db_config.get("password"))
|
||||
database = db_config.get("database")
|
||||
return f"psycopg2+postgresql://{username}:{password}@{hostname}:{port}/{database}"
|
||||
else:
|
||||
print(f"Error: unknown database type '{db_config.get('type')}'")
|
||||
exit(1)
|
||||
|
||||
|
||||
@classmethod
|
||||
def debug(cls) -> str:
|
||||
return pformat(cls._config)
|
||||
|
||||
|
||||
@classmethod
|
||||
def load_configuration(cls, args: dict[str, any]) -> dict[str, any]:
|
||||
cls._config = cls._load_configuration_from_file(args.get('config_file'))
|
||||
cls._config = cls._load_configuration_from_file(args.get("config_file"))
|
35
src/worblehat/services/email.py
Normal file
35
src/worblehat/services/email.py
Normal file
@ -0,0 +1,35 @@
|
||||
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(), " "))
|
67
src/worblehat/services/metadata_fetchers/BookMetadata.py
Normal file
67
src/worblehat/services/metadata_fetchers/BookMetadata.py
Normal file
@ -0,0 +1,67 @@
|
||||
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}")
|
@ -0,0 +1,21 @@
|
||||
# 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
|
@ -0,0 +1,51 @@
|
||||
"""
|
||||
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)
|
@ -0,0 +1,70 @@
|
||||
"""
|
||||
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)
|
@ -0,0 +1,109 @@
|
||||
"""
|
||||
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)
|
3
src/worblehat/services/metadata_fetchers/__init__.py
Normal file
3
src/worblehat/services/metadata_fetchers/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .book_metadata_fetcher import fetch_metadata_from_multiple_sources
|
||||
|
||||
__all__ = ["fetch_metadata_from_multiple_sources"]
|
@ -0,0 +1,85 @@
|
||||
"""
|
||||
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)
|
267
src/worblehat/services/seed_test_data.py
Normal file
267
src/worblehat/services/seed_test_data.py
Normal file
@ -0,0 +1,267 @@
|
||||
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.")
|
494
uv.lock
generated
Normal file
494
uv.lock
generated
Normal file
@ -0,0 +1,494 @@
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mako" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/09/f844822e4e847a3f0bd41797f93c4674cd4d2462a3f6c459aa528cdf786e/alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213", size = 1918219 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7e/ac0991d1745f7d755fc1cd381b3990a45b404b4d008fc75e2a983516fbfe/alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", size = 233565 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.13.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 },
|
||||
]
|
||||
|
||||
[[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.1.31"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
|
||||
]
|
||||
|
||||
[[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blinker" },
|
||||
{ name = "click" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-admin"
|
||||
version = "1.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "wtforms" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/4d/7cad383a93e3e1dd9378f1fcf05ddc532c6d921fb30c19ce8f8583630f24/Flask-Admin-1.6.1.tar.gz", hash = "sha256:24cae2af832b6a611a01d7dc35f42d266c1d6c75a426b869d8cb241b78233369", size = 6651224 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/b3/656c78dfef163517dbbc9fd106f0604e37b436ad51f9d9450b60e9407e35/Flask_Admin-1.6.1-py3-none-any.whl", hash = "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406", size = 7498141 },
|
||||
]
|
||||
|
||||
[[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/ce/1eb1e2afaca49add3185e202ce9c2c6d5779b7eecb20973e43ad804eb2a4/isbnlib-3.10.14-py2.py3-none-any.whl", hash = "sha256:f885b350fc8e600a919ed46e3b07253062cd604af69885455a25a299217b3fe2", size = 52535 },
|
||||
]
|
||||
|
||||
[[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libdib"
|
||||
version = "0.1.0"
|
||||
source = { git = "https://git.pvv.ntnu.no/Projects/libdib.git#240424b3f30f4c41d1edae72116904697b71e823" }
|
||||
dependencies = [
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/4f/ddb1965901bc388958db9f0c991255b2c469349a741ae8c9cd8a562d70a6/mako-1.3.9.tar.gz", hash = "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac", size = 392195 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/83/de0a49e7de540513f53ab5d2e105321dedeb08a8f5850f0208decf4390ec/Mako-1.3.9-py3-none-any.whl", hash = "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", size = 78456 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
|
||||
]
|
||||
|
||||
[[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poethepoet"
|
||||
version = "0.32.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pastel" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/c6/4bc7e21166726fc96f82f58b31fd032fdf8864d3aa17e2622578cb96c24d/poethepoet-0.32.2.tar.gz", hash = "sha256:1d68871dac1b191e27bd68fea57d0e01e9afbba3fcd01dbe6f6bc3fcb071fe4c", size = 61381 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1f/4e7a9b6b33a085172a826d1f9d0a19a2e77982298acea13d40442f14ef28/poethepoet-0.32.2-py3-none-any.whl", hash = "sha256:97e165de8e00b07d33fd8d72896fad8b20ccafcd327b1118bb6a3da26af38d33", size = 81726 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.39"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/8e/e77fcaa67f8b9f504b4764570191e291524575ddbfe78a90fc656d671fdc/sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22", size = 9644602 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/86/b2cb432aeb00a1eda7ed33ce86d943c2452dc1642f3ec51bfe9eaae9604b/sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b", size = 2107210 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b0/b2479edb3419ca763ba1b587161c292d181351a33642985506a530f9162b/sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c", size = 2097599 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/5e/c5b792a4abcc71e68d44cb531c4845ac539d558975cc61db1afbc8a73c96/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47", size = 3247012 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/a8/055fa8a7c5f85e6123b7e40ec2e9e87d63c566011d599b4a5ab75e033017/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd", size = 3257851 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/40/aec16681e91a22ddf03dbaeb3c659bce96107c5f47d2a7c665eb7f24a014/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a", size = 3193155 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/9d/cef697b137b9eb0b66ab8e9cf193a7c7c048da3b4bb667e5fcea4d90c7a2/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06", size = 3219770 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/05/e109ca7dde837d8f2f1b235357e4e607f8af81ad8bc29c230fed8245687d/sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109", size = 2077567 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c6/25ca068e38c29ed6be0fde2521888f19da923dbd58f5ff16af1b73ec9b58/sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338", size = 2103136 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/47/55778362642344324a900b6b2b1b26f7f02225b374eb93adc4a363a2d8ae/sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87", size = 2102484 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/e1/f5f26f67d095f408138f0fb2c37f827f3d458f2ae51881546045e7e55566/sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716", size = 2092955 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/c2/0db0022fc729a54fc7aef90a3457bf20144a681baef82f7357832b44c566/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4", size = 3179367 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/b7/f33743d87d0b4e7a1f12e1631a4b9a29a8d0d7c0ff9b8c896d0bf897fb60/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d", size = 3192705 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/74/6814f31719109c973ddccc87bdfc2c2a9bc013bec64a375599dc5269a310/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e", size = 3125927 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/6b/18f476f4baaa9a0e2fbc6808d8f958a5268b637c8eccff497bf96908d528/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723", size = 3154055 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/60/76714cecb528da46bc53a0dd36d1ccef2f74ef25448b630a0a760ad07bdb/sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7", size = 2075315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/7c/76828886d913700548bac5851eefa5b2c0251ebc37921fe476b93ce81b50/sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0", size = 2099175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/0f/d69904cb7d17e65c65713303a244ec91fd3c96677baf1d6331457fd47e16/sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f", size = 1898621 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
|
||||
]
|
||||
|
||||
[[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.13.3" },
|
||||
{ name = "beautifulsoup4", specifier = ">=4.12.3" },
|
||||
{ name = "click", specifier = ">=8.1.7" },
|
||||
{ name = "flask", specifier = ">=3.0.3" },
|
||||
{ name = "flask-admin", specifier = ">=1.6.1" },
|
||||
{ name = "flask-sqlalchemy", specifier = ">=3.1.1" },
|
||||
{ name = "isbnlib", specifier = ">=3.10.14" },
|
||||
{ name = "libdib", git = "https://git.pvv.ntnu.no/Projects/libdib.git" },
|
||||
{ name = "psycopg2-binary", specifier = ">=2.9.9" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.34" },
|
||||
]
|
||||
|
||||
[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 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454 },
|
||||
]
|
@ -1 +0,0 @@
|
||||
from .main import WorblehatCli
|
@ -1,234 +0,0 @@
|
||||
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',
|
||||
},
|
||||
}
|
@ -1,211 +0,0 @@
|
||||
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
|
@ -1,111 +0,0 @@
|
||||
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',
|
||||
},
|
||||
}
|
@ -1,346 +0,0 @@
|
||||
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 +0,0 @@
|
||||
from .main import DeadlineDaemon
|
@ -1,147 +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):
|
||||
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')
|
@ -1,18 +0,0 @@
|
||||
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,73 +0,0 @@
|
||||
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,175 +0,0 @@
|
||||
"""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 ###
|
@ -1,15 +0,0 @@
|
||||
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)
|
@ -1,15 +0,0 @@
|
||||
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)
|
@ -1,2 +0,0 @@
|
||||
from .Item_Author import Item_Author
|
||||
from .Item_Category import Item_Category
|
@ -1,8 +0,0 @@
|
||||
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
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user