Compare commits
19 Commits
40
README.md
40
README.md
|
@ -43,43 +43,3 @@ Unless provided through the `--config` flag, program will automatically look for
|
||||||
- `/var/lib/worblehat/config.toml`
|
- `/var/lib/worblehat/config.toml`
|
||||||
|
|
||||||
Run `poetry run worblehat --help` for more info
|
Run `poetry run worblehat --help` for more info
|
||||||
|
|
||||||
## TODO List
|
|
||||||
|
|
||||||
### Setting up a database with all of PVVs books
|
|
||||||
|
|
||||||
- [ ] Create postgres database
|
|
||||||
- [ ] Model all bookshelfs
|
|
||||||
- [ ] Scan in all books
|
|
||||||
|
|
||||||
### Cli version of the program (this is currently being worked on)
|
|
||||||
|
|
||||||
- [X] Ability to pull data from online sources with ISBN
|
|
||||||
- [X] Ability to create and update bookcases
|
|
||||||
- [X] Ability to create and update bookcase shelfs
|
|
||||||
- [X] Ability to create and update bookcase items
|
|
||||||
- [X] Ability to borrow and deliver items
|
|
||||||
- [ ] Ability to borrow and deliver multiple items at a time
|
|
||||||
- [X] Ability to enter the queue for borrowing an item
|
|
||||||
- [ ] Ability to extend a borrowing, only if no one is behind you in the queue
|
|
||||||
- [ ] Ability to list borrowed items which are overdue
|
|
||||||
- [~] Ability to search for items
|
|
||||||
- [ ] Ability to print PVV-specific labels for items missing a label, or which for any other reason needs a custom one
|
|
||||||
- [X] Ascii art of monkey with fingers in eyes
|
|
||||||
|
|
||||||
### Deadline daemon
|
|
||||||
|
|
||||||
- [X] Ability to be notified when deadlines are due
|
|
||||||
- [ ] Ability to be notified when books are available
|
|
||||||
- [ ] Ability to have expiring queue positions automatically expire
|
|
||||||
|
|
||||||
### Web version of the program
|
|
||||||
|
|
||||||
- [ ] Ability for PVV members to search for books through the PVV website
|
|
||||||
|
|
||||||
## Points of discussion
|
|
||||||
|
|
||||||
- Should this project run in a separate tty-instance on Dibblers interface, or should they share the tty with some kind of switching ability?
|
|
||||||
After some discussion with other PVV members, we came up with an idea where we run the programs in separate ttys, and use a set of large mechanical switches connected to a QMK-flashed microcontroller to switch between them.
|
|
||||||
|
|
||||||
- Workaround for not being able to represent items with same ISBN and different owner: if you are absolutely adamant about placing your item at PVV while still owning it, even though PVV already owns a copy of this item, please print out a new label with a "PVV-ISBN" for it
|
|
||||||
|
|
|
@ -33,5 +33,8 @@ from = 'worblehat@pvv.ntnu.no'
|
||||||
subject_prefix = '[Worblehat]'
|
subject_prefix = '[Worblehat]'
|
||||||
|
|
||||||
[deadline_daemon]
|
[deadline_daemon]
|
||||||
warn_days_before_borrow_deadline = [ "5", "1" ]
|
enabled = true
|
||||||
warn_days_before_expiring_queue_position_deadline = [ "3", "1" ]
|
dryrun = false
|
||||||
|
warn_days_before_borrowing_deadline = [ 5, 1 ]
|
||||||
|
days_before_queue_position_expires = 14
|
||||||
|
warn_days_before_expiring_queue_position_deadline = [ 3, 1 ]
|
|
@ -12,14 +12,10 @@ isbn, note, bookcase, shelf
|
||||||
9781292024967,abstract alg,arbeidsrom_smal,5
|
9781292024967,abstract alg,arbeidsrom_smal,5
|
||||||
9780471728979,kreyzig,arbeidsrom_smal,5
|
9780471728979,kreyzig,arbeidsrom_smal,5
|
||||||
9781847762399,calc 1,arbeidsrom_smal,5
|
9781847762399,calc 1,arbeidsrom_smal,5
|
||||||
9781847762399, calc 1 again, arbeidsrom_smal, 5
|
|
||||||
9781787267763,calc 1 nome,arbeidsrom_smal,5
|
9781787267763,calc 1 nome,arbeidsrom_smal,5
|
||||||
9781787267770,calc 2 nome,arbeidsrom_smal,5
|
9781787267770,calc 2 nome,arbeidsrom_smal,5
|
||||||
9780199208258,non lin ode,arbeidsrom_smal,5
|
9780199208258,non lin ode,arbeidsrom_smal,5
|
||||||
9788251915953,tabeller,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
|
9780750304009,fractals and chaos,arbeidsrom_smal,5
|
||||||
9788241902116,geometri,arbeidsrom_smal,5
|
9788241902116,geometri,arbeidsrom_smal,5
|
||||||
9781620402788,simpsons,arbeidsrom_smal,5
|
9781620402788,simpsons,arbeidsrom_smal,5
|
||||||
|
|
|
|
@ -2,16 +2,16 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1683014792,
|
"lastModified": 1714272655,
|
||||||
"narHash": "sha256-6Va9iVtmmsw4raBc3QKvQT2KT/NGRWlvUlJj46zN8B8=",
|
"narHash": "sha256-3/ghIWCve93ngkx5eNPdHIKJP/pMzSr5Wc4rNKE1wOc=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "1a411f23ba299db155a5b45d5e145b85a7aafc42",
|
"rev": "12430e43bd9b81a6b4e79e64f87c624ade701eaf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"id": "nixpkgs",
|
||||||
"ref": "nixos-unstable",
|
"ref": "nixos-23.11",
|
||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
41
flake.nix
41
flake.nix
|
@ -1,23 +1,42 @@
|
||||||
{
|
{
|
||||||
# inputs.nixpkgs.url = "nixpkgs/nixos-22.11";
|
inputs.nixpkgs.url = "nixpkgs/nixos-23.11";
|
||||||
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs }: let
|
outputs = { self, nixpkgs }: let
|
||||||
system = "x86_64-linux";
|
systems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
inherit (pkgs) lib;
|
in f system pkgs);
|
||||||
in {
|
in {
|
||||||
apps.${system} = let
|
apps = forAllSystems (system: pkgs: let
|
||||||
app = program: {
|
mkApp = program: {
|
||||||
type = "app";
|
type = "app";
|
||||||
inherit program;
|
inherit program;
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
default = self.apps.${system}.worblehat;
|
default = mkApp self.packages.${system}.default;
|
||||||
worblehat = app "${self.packages.${system}.worblehat}/bin/worblehat";
|
});
|
||||||
};
|
|
||||||
|
|
||||||
packages.${system} = {
|
devShells = forAllSystems (_: pkgs: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
poetry
|
||||||
|
sqlite
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
poetry install
|
||||||
|
poetry shell && exit
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
overlays.default = final: prev: self.packages.${final.system};
|
||||||
|
|
||||||
|
packages = forAllSystems (system: pkgs: {
|
||||||
default = self.packages.${system}.worblehat;
|
default = self.packages.${system}.worblehat;
|
||||||
worblehat = with pkgs.python3Packages; buildPythonPackage {
|
worblehat = with pkgs.python3Packages; buildPythonPackage {
|
||||||
pname = "worblehat";
|
pname = "worblehat";
|
||||||
|
@ -38,6 +57,6 @@
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -1,126 +1,4 @@
|
||||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aiohttp"
|
|
||||||
version = "3.8.5"
|
|
||||||
description = "Async http client/server framework (asyncio)"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
files = [
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"},
|
|
||||||
{file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"},
|
|
||||||
{file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"},
|
|
||||||
{file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"},
|
|
||||||
{file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"},
|
|
||||||
{file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"},
|
|
||||||
{file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"},
|
|
||||||
{file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
aiosignal = ">=1.1.2"
|
|
||||||
async-timeout = ">=4.0.0a3,<5.0"
|
|
||||||
attrs = ">=17.3.0"
|
|
||||||
charset-normalizer = ">=2.0,<4.0"
|
|
||||||
frozenlist = ">=1.1.1"
|
|
||||||
multidict = ">=4.5,<7.0"
|
|
||||||
yarl = ">=1.0,<2.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
speedups = ["Brotli", "aiodns", "cchardet"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aiosignal"
|
|
||||||
version = "1.3.1"
|
|
||||||
description = "aiosignal: a list of registered asynchronous callbacks"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
|
|
||||||
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
frozenlist = ">=1.1.0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
|
@ -141,50 +19,24 @@ typing-extensions = ">=4"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
tz = ["python-dateutil"]
|
tz = ["python-dateutil"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-timeout"
|
|
||||||
version = "4.0.3"
|
|
||||||
description = "Timeout context manager for asyncio programs"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
|
|
||||||
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "attrs"
|
|
||||||
version = "23.1.0"
|
|
||||||
description = "Classes Without Boilerplate"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
|
|
||||||
{file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
|
|
||||||
dev = ["attrs[docs,tests]", "pre-commit"]
|
|
||||||
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
|
|
||||||
tests = ["attrs[tests-no-zope]", "zope-interface"]
|
|
||||||
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "beautifulsoup4"
|
name = "beautifulsoup4"
|
||||||
version = "4.12.2"
|
version = "4.12.3"
|
||||||
description = "Screen-scraping library"
|
description = "Screen-scraping library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6.0"
|
python-versions = ">=3.6.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"},
|
{file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
|
||||||
{file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"},
|
{file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
soupsieve = ">1.2"
|
soupsieve = ">1.2"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
cchardet = ["cchardet"]
|
||||||
|
chardet = ["chardet"]
|
||||||
|
charset-normalizer = ["charset-normalizer"]
|
||||||
html5lib = ["html5lib"]
|
html5lib = ["html5lib"]
|
||||||
lxml = ["lxml"]
|
lxml = ["lxml"]
|
||||||
|
|
||||||
|
@ -201,12 +53,13 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bs4"
|
name = "bs4"
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
description = "Dummy package for Beautiful Soup"
|
description = "Dummy package for Beautiful Soup (beautifulsoup4)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"},
|
{file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"},
|
||||||
|
{file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -214,97 +67,112 @@ beautifulsoup4 = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2023.7.22"
|
version = "2024.7.4"
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
files = [
|
files = [
|
||||||
{file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
|
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||||
{file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
|
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "3.2.0"
|
version = "3.3.2"
|
||||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.0"
|
python-versions = ">=3.7.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
|
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
|
||||||
{file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
|
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
|
||||||
{file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
|
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
|
||||||
{file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
|
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
|
||||||
{file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
|
||||||
{file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
|
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
|
||||||
{file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
|
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
|
||||||
|
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -388,76 +256,6 @@ files = [
|
||||||
Flask = ">=2.2"
|
Flask = ">=2.2"
|
||||||
SQLAlchemy = ">=1.4.18"
|
SQLAlchemy = ">=1.4.18"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "frozenlist"
|
|
||||||
version = "1.4.0"
|
|
||||||
description = "A list-like structure which implements collections.abc.MutableSequence"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-win32.whl", hash = "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0"},
|
|
||||||
{file = "frozenlist-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431"},
|
|
||||||
{file = "frozenlist-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-win32.whl", hash = "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc"},
|
|
||||||
{file = "frozenlist-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-win32.whl", hash = "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f"},
|
|
||||||
{file = "frozenlist-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167"},
|
|
||||||
{file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
|
@ -470,6 +268,7 @@ files = [
|
||||||
{file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
|
{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-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-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
|
||||||
|
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"},
|
||||||
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
|
{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_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_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
|
||||||
|
@ -478,6 +277,7 @@ files = [
|
||||||
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
|
{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-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-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
|
||||||
|
{file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"},
|
||||||
{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_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_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-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
|
||||||
|
@ -507,6 +307,7 @@ files = [
|
||||||
{file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
|
{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-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-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
|
||||||
|
{file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
|
{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_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_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
|
||||||
|
@ -515,6 +316,7 @@ files = [
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
|
{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-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
|
||||||
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
|
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
|
||||||
|
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"},
|
||||||
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
|
{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-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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
|
||||||
|
@ -533,13 +335,13 @@ test = ["objgraph", "psutil"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.4"
|
version = "3.7"
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
files = [
|
files = [
|
||||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -659,89 +461,6 @@ files = [
|
||||||
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
|
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "multidict"
|
|
||||||
version = "6.0.4"
|
|
||||||
description = "multidict implementation"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"},
|
|
||||||
{file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"},
|
|
||||||
{file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"},
|
|
||||||
{file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"},
|
|
||||||
{file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"},
|
|
||||||
{file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"},
|
|
||||||
{file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pastel"
|
name = "pastel"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -844,13 +563,13 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.31.0"
|
version = "2.32.3"
|
||||||
description = "Python HTTP for Humans."
|
description = "Python HTTP for Humans."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
|
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||||
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
@ -865,13 +584,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "soupsieve"
|
name = "soupsieve"
|
||||||
version = "2.4.1"
|
version = "2.5"
|
||||||
description = "A modern CSS selector implementation for Beautiful Soup."
|
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"},
|
{file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
|
||||||
{file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"},
|
{file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -951,28 +670,6 @@ postgresql-psycopg2cffi = ["psycopg2cffi"]
|
||||||
pymysql = ["pymysql"]
|
pymysql = ["pymysql"]
|
||||||
sqlcipher = ["sqlcipher3-binary"]
|
sqlcipher = ["sqlcipher3-binary"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thestorygraph-client"
|
|
||||||
version = "0.1.6"
|
|
||||||
description = "A Simple Python Client for TheStoryGraph"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.9"
|
|
||||||
files = [
|
|
||||||
{file = "thestorygraph-client-0.1.6.tar.gz", hash = "sha256:f372bb63f5e172d69cec4a588a74ce16cd97212936847d668b75d4eff4c33be4"},
|
|
||||||
{file = "thestorygraph_client-0.1.6-py3-none-any.whl", hash = "sha256:0c1afa730a3a1d659cea86f147df0bb840c3fae8b22e5c3a3fa21295100d122e"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
aiohttp = "*"
|
|
||||||
attrs = "*"
|
|
||||||
beautifulsoup4 = "*"
|
|
||||||
requests = "*"
|
|
||||||
yarl = "*"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
build = ["build", "twine"]
|
|
||||||
testing = ["mypy", "pytest", "types-requests"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
|
@ -997,18 +694,18 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.0.4"
|
version = "2.2.2"
|
||||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"},
|
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
||||||
{file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"},
|
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||||
secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
|
h2 = ["h2 (>=4,<5)"]
|
||||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
zstd = ["zstandard (>=0.18.0)"]
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
|
@ -1046,94 +743,7 @@ MarkupSafe = "*"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
email = ["email-validator"]
|
email = ["email-validator"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yarl"
|
|
||||||
version = "1.9.2"
|
|
||||||
description = "Yet another URL library"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"},
|
|
||||||
{file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"},
|
|
||||||
{file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"},
|
|
||||||
{file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"},
|
|
||||||
{file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"},
|
|
||||||
{file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"},
|
|
||||||
{file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
idna = ">=2.0"
|
|
||||||
multidict = ">=4.0"
|
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "a3c3292966b347841c706a20a0bec053982b73c65d252a2318abbec9cd1bd129"
|
content-hash = "4498f324fc36a557e7b63b693fea8a11388b127e86dcc17ab67e087dce3a9ec3"
|
||||||
|
|
|
@ -16,9 +16,8 @@ isbnlib = "^3.10.14"
|
||||||
python = "^3.11"
|
python = "^3.11"
|
||||||
sqlalchemy = "^2.0.8"
|
sqlalchemy = "^2.0.8"
|
||||||
psycopg2-binary = "^2.9.6"
|
psycopg2-binary = "^2.9.6"
|
||||||
requests = "^2.31.0"
|
requests = "^2.32.3"
|
||||||
bs4 = "^0.0.1"
|
bs4 = "^0.0.2"
|
||||||
thestorygraph-client = "^0.1.6"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
werkzeug = "^2.3.3"
|
werkzeug = "^2.3.3"
|
||||||
|
|
|
@ -61,26 +61,6 @@ class WorblehatCli(NumberedCmd):
|
||||||
exit(0)
|
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):
|
def do_show_bookcase(self, arg: str):
|
||||||
bookcase_selector = InteractiveItemSelector(
|
bookcase_selector = InteractiveItemSelector(
|
||||||
cls = Bookcase,
|
cls = Bookcase,
|
||||||
|
@ -95,6 +75,35 @@ class WorblehatCli(NumberedCmd):
|
||||||
print(f' {item.name} - {item.amount} copies')
|
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):
|
def _create_bookcase_item(self, isbn: str):
|
||||||
bookcase_item = create_bookcase_item_from_isbn(isbn, self.sql_session)
|
bookcase_item = create_bookcase_item_from_isbn(isbn, self.sql_session)
|
||||||
if bookcase_item is None:
|
if bookcase_item is None:
|
||||||
|
@ -149,6 +158,8 @@ class WorblehatCli(NumberedCmd):
|
||||||
if (existing_item := self.sql_session.scalars(
|
if (existing_item := self.sql_session.scalars(
|
||||||
select(BookcaseItem)
|
select(BookcaseItem)
|
||||||
.where(BookcaseItem.isbn == isbn)
|
.where(BookcaseItem.isbn == isbn)
|
||||||
|
.join(BookcaseItemBorrowing)
|
||||||
|
.join(BookcaseItemBorrowingQueue)
|
||||||
).one_or_none()) is not None:
|
).one_or_none()) is not None:
|
||||||
print(f'\nFound existing item for isbn "{isbn}"')
|
print(f'\nFound existing item for isbn "{isbn}"')
|
||||||
BookcaseItemCli(
|
BookcaseItemCli(
|
||||||
|
@ -171,6 +182,28 @@ class WorblehatCli(NumberedCmd):
|
||||||
).cmdloop()
|
).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):
|
def do_advanced(self, _: str):
|
||||||
AdvancedOptionsCli(self.sql_session).cmdloop()
|
AdvancedOptionsCli(self.sql_session).cmdloop()
|
||||||
|
|
||||||
|
@ -204,26 +237,30 @@ class WorblehatCli(NumberedCmd):
|
||||||
'doc': 'Choose / Add item with its ISBN',
|
'doc': 'Choose / Add item with its ISBN',
|
||||||
},
|
},
|
||||||
1: {
|
1: {
|
||||||
'f': do_list_bookcases,
|
|
||||||
'doc': 'List all bookcases',
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
'f': do_search,
|
'f': do_search,
|
||||||
'doc': 'Search',
|
'doc': 'Search',
|
||||||
},
|
},
|
||||||
3: {
|
2: {
|
||||||
'f': do_show_bookcase,
|
'f': do_show_bookcase,
|
||||||
'doc': 'Show a bookcase, and its items',
|
'doc': 'Show a bookcase, and its items',
|
||||||
},
|
},
|
||||||
|
3: {
|
||||||
|
'f': do_show_borrowed_queued,
|
||||||
|
'doc': 'Show borrowed/queued items',
|
||||||
|
},
|
||||||
4: {
|
4: {
|
||||||
|
'f': do_show_slabbedasker,
|
||||||
|
'doc': 'Show slabbedasker',
|
||||||
|
},
|
||||||
|
5: {
|
||||||
'f': do_save,
|
'f': do_save,
|
||||||
'doc': 'Save changes',
|
'doc': 'Save changes',
|
||||||
},
|
},
|
||||||
5: {
|
6: {
|
||||||
'f': do_abort,
|
'f': do_abort,
|
||||||
'doc': 'Abort changes',
|
'doc': 'Abort changes',
|
||||||
},
|
},
|
||||||
6: {
|
7: {
|
||||||
'f': do_advanced,
|
'f': do_advanced,
|
||||||
'doc': 'Advanced options',
|
'doc': 'Advanced options',
|
||||||
},
|
},
|
||||||
|
|
|
@ -58,6 +58,7 @@ class InteractiveItemSelector(Cmd):
|
||||||
self.execute_selection = execute_selection
|
self.execute_selection = execute_selection
|
||||||
self.complete_selection = complete_selection
|
self.complete_selection = complete_selection
|
||||||
self.default_item = default
|
self.default_item = default
|
||||||
|
self.result = None
|
||||||
|
|
||||||
if default is not None:
|
if default is not None:
|
||||||
self.prompt = f'Select {cls.__name__} [{default.name}]> '
|
self.prompt = f'Select {cls.__name__} [{default.name}]> '
|
||||||
|
@ -197,6 +198,7 @@ class NumberedItemSelector(NumberedCmd):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.items = items
|
self.items = items
|
||||||
self.stringify = stringify
|
self.stringify = stringify
|
||||||
|
self.result = None
|
||||||
self.funcs = {
|
self.funcs = {
|
||||||
i: {
|
i: {
|
||||||
'f': self._select_item,
|
'f': self._select_item,
|
||||||
|
|
|
@ -91,6 +91,26 @@ class AdvancedOptionsCli(NumberedCmd):
|
||||||
self.sql_session.flush()
|
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):
|
def do_done(self, _: str):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -104,6 +124,10 @@ class AdvancedOptionsCli(NumberedCmd):
|
||||||
'f': do_add_bookcase_shelf,
|
'f': do_add_bookcase_shelf,
|
||||||
'doc': 'Add bookcase shelf',
|
'doc': 'Add bookcase shelf',
|
||||||
},
|
},
|
||||||
|
3: {
|
||||||
|
'f': do_list_bookcases,
|
||||||
|
'doc': 'List all bookcases',
|
||||||
|
},
|
||||||
9: {
|
9: {
|
||||||
'f': do_done,
|
'f': do_done,
|
||||||
'doc': 'Done',
|
'doc': 'Done',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
|
||||||
from worblehat.cli.prompt_utils import (
|
from worblehat.cli.prompt_utils import (
|
||||||
InteractiveItemSelector,
|
InteractiveItemSelector,
|
||||||
NumberedCmd,
|
NumberedCmd,
|
||||||
|
NumberedItemSelector,
|
||||||
format_date,
|
format_date,
|
||||||
prompt_yes_no,
|
prompt_yes_no,
|
||||||
)
|
)
|
||||||
|
@ -22,6 +23,7 @@ from worblehat.services.bookcase_item import (
|
||||||
create_bookcase_item_from_isbn,
|
create_bookcase_item_from_isbn,
|
||||||
is_valid_isbn,
|
is_valid_isbn,
|
||||||
)
|
)
|
||||||
|
from worblehat.services.config import Config
|
||||||
|
|
||||||
from .bookcase_shelf_selector import select_bookcase_shelf
|
from .bookcase_shelf_selector import select_bookcase_shelf
|
||||||
|
|
||||||
|
@ -174,6 +176,50 @@ class BookcaseItemCli(NumberedCmd):
|
||||||
print(f'Successfully delivered the item for {borrowing.username}')
|
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):
|
def do_done(self, _: str):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -188,10 +234,14 @@ class BookcaseItemCli(NumberedCmd):
|
||||||
'doc': 'Deliver',
|
'doc': 'Deliver',
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
|
'f': do_extend_borrowing,
|
||||||
|
'doc': 'Extend borrowing',
|
||||||
|
},
|
||||||
|
4: {
|
||||||
'f': do_edit,
|
'f': do_edit,
|
||||||
'doc': 'Edit',
|
'doc': 'Edit',
|
||||||
},
|
},
|
||||||
4: {
|
5: {
|
||||||
'f': do_update_data,
|
'f': do_update_data,
|
||||||
'doc': 'Pull updated data from online databases',
|
'doc': 'Pull updated data from online databases',
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,6 +13,7 @@ class SearchCli(NumberedCmd):
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.sql_session = sql_session
|
self.sql_session = sql_session
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
|
||||||
def do_search_all(self, _: str):
|
def do_search_all(self, _: str):
|
||||||
|
@ -57,11 +58,11 @@ class SearchCli(NumberedCmd):
|
||||||
elif len(author) == 1:
|
elif len(author) == 1:
|
||||||
selected_author = author[0]
|
selected_author = author[0]
|
||||||
print('Found author:')
|
print('Found author:')
|
||||||
print(f" {selected_author.name} ({sum(item.amount for item in selected_author.books)} items)")
|
print(f" {selected_author.name} ({sum(item.amount for item in selected_author.items)} items)")
|
||||||
else:
|
else:
|
||||||
selector = NumberedItemSelector(
|
selector = NumberedItemSelector(
|
||||||
items = author,
|
items = author,
|
||||||
stringify = lambda author: f"{author.name} ({sum(item.amount for item in author.books)} items)",
|
stringify = lambda author: f"{author.name} ({sum(item.amount for item in author.items)} items)",
|
||||||
)
|
)
|
||||||
selector.cmdloop()
|
selector.cmdloop()
|
||||||
if selector.result is None:
|
if selector.result is None:
|
||||||
|
@ -69,7 +70,7 @@ class SearchCli(NumberedCmd):
|
||||||
selected_author = selector.result
|
selected_author = selector.result
|
||||||
|
|
||||||
selector = NumberedItemSelector(
|
selector = NumberedItemSelector(
|
||||||
items = selected_author.books,
|
items = list(selected_author.items),
|
||||||
stringify = lambda item: f"{item.name} ({item.isbn})",
|
stringify = lambda item: f"{item.name} ({item.isbn})",
|
||||||
)
|
)
|
||||||
selector.cmdloop()
|
selector.cmdloop()
|
||||||
|
|
|
@ -11,14 +11,25 @@ from worblehat.models import (
|
||||||
DeadlineDaemonLastRunDatetime,
|
DeadlineDaemonLastRunDatetime,
|
||||||
BookcaseItemBorrowingQueue,
|
BookcaseItemBorrowingQueue,
|
||||||
)
|
)
|
||||||
|
|
||||||
from worblehat.services.email import send_email
|
from worblehat.services.email import send_email
|
||||||
|
|
||||||
class DeadlineDaemon:
|
class DeadlineDaemon:
|
||||||
def __init__(self, sql_session: Session):
|
def __init__(self, sql_session: Session):
|
||||||
|
if not Config['deadline_daemon.enabled']:
|
||||||
|
return
|
||||||
|
|
||||||
self.sql_session = sql_session
|
self.sql_session = sql_session
|
||||||
|
|
||||||
self.last_run = self.sql_session.scalars(
|
self.last_run = self.sql_session.scalars(
|
||||||
select(DeadlineDaemonLastRunDatetime),
|
select(DeadlineDaemonLastRunDatetime),
|
||||||
).one()
|
).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.last_run_datetime = self.last_run.time
|
||||||
self.current_run_datetime = datetime.now()
|
self.current_run_datetime = datetime.now()
|
||||||
|
@ -26,6 +37,12 @@ class DeadlineDaemon:
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
logging.info('Deadline daemon started')
|
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_close_deadline_reminder_mails()
|
||||||
self.send_overdue_mails()
|
self.send_overdue_mails()
|
||||||
|
@ -36,6 +53,97 @@ class DeadlineDaemon:
|
||||||
self.last_run.time = self.current_run_datetime
|
self.last_run.time = self.current_run_datetime
|
||||||
self.sql_session.commit()
|
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):
|
def _sql_subtract_date(self, x: datetime, y: timedelta):
|
||||||
if self.sql_session.bind.dialect.name == 'sqlite':
|
if self.sql_session.bind.dialect.name == 'sqlite':
|
||||||
|
@ -48,10 +156,10 @@ class DeadlineDaemon:
|
||||||
|
|
||||||
|
|
||||||
def send_close_deadline_reminder_mails(self):
|
def send_close_deadline_reminder_mails(self):
|
||||||
logging.info('Sending mails about items with a closing deadline')
|
logging.info('Sending mails for items with a closing deadline')
|
||||||
|
|
||||||
# TODO: This should be int-parsed and validated before the daemon started
|
# 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']]
|
days = [int(d) for d in Config['deadline_daemon.warn_days_before_borrowing_deadline']]
|
||||||
|
|
||||||
for day in days:
|
for day in days:
|
||||||
borrowings_to_remind = self.sql_session.scalars(
|
borrowings_to_remind = self.sql_session.scalars(
|
||||||
|
@ -69,23 +177,11 @@ class DeadlineDaemon:
|
||||||
),
|
),
|
||||||
).all()
|
).all()
|
||||||
for borrowing in borrowings_to_remind:
|
for borrowing in borrowings_to_remind:
|
||||||
logging.info(f' Sending close deadline mail to {borrowing.username}@pvv.ntnu.no. {day} days left')
|
self._send_close_deadline_mail(borrowing)
|
||||||
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):
|
def send_overdue_mails(self):
|
||||||
logging.info('Sending mails about overdue items')
|
logging.info('Sending mails for overdue items')
|
||||||
|
|
||||||
to_remind = self.sql_session.scalars(
|
to_remind = self.sql_session.scalars(
|
||||||
select(BookcaseItemBorrowing)
|
select(BookcaseItemBorrowing)
|
||||||
|
@ -96,19 +192,7 @@ class DeadlineDaemon:
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
for borrowing in to_remind:
|
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")}')
|
self._send_overdue_mail(borrowing)
|
||||||
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):
|
def send_newly_available_mails(self):
|
||||||
|
@ -133,15 +217,75 @@ class DeadlineDaemon:
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
for queue_item in newly_available:
|
for queue_item in newly_available:
|
||||||
logging.info(f'Sending newly available mail to {queue_item.username}')
|
logging.info(f'Adding user {queue_item.username} to queue for {queue_item.item.name}')
|
||||||
logging.warning('Not implemented')
|
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):
|
def send_expiring_queue_position_mails(self):
|
||||||
logging.info('Sending mails about queue positions which are expiring soon')
|
logging.info('Sending mails about queue positions which are expiring soon')
|
||||||
logging.warning('Not implemented')
|
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):
|
def auto_expire_queue_positions(self):
|
||||||
logging.info('Expiring queue positions which are too old')
|
logging.info('Expiring queue positions which are too old')
|
||||||
logging.warning('Not implemented')
|
|
||||||
|
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()
|
|
@ -0,0 +1,117 @@
|
||||||
|
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()
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import csv
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from worblehat.models import (
|
||||||
|
Bookcase,
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseShelf,
|
||||||
|
MediaType,
|
||||||
|
Language,
|
||||||
|
)
|
||||||
|
from worblehat.services.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
CSV_FILE = Path(__file__).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=row[0],
|
||||||
|
name=row[1],
|
||||||
|
)
|
||||||
|
item.media_type = media_type
|
||||||
|
item.language = language
|
||||||
|
bookcase_items.append(item)
|
||||||
|
|
||||||
|
half = len(bookcase_items) // 2
|
||||||
|
first_half = bookcase_items[:half]
|
||||||
|
second_half = bookcase_items[half:]
|
||||||
|
|
||||||
|
for item in first_half:
|
||||||
|
seed_shelf_1.items.add(item)
|
||||||
|
|
||||||
|
for item in second_half:
|
||||||
|
seed_shelf_2.items.add(item)
|
||||||
|
|
||||||
|
sql_session.add_all(bookcase_items)
|
||||||
|
sql_session.commit()
|
|
@ -1,432 +0,0 @@
|
||||||
openapi: 3.0.3
|
|
||||||
info:
|
|
||||||
title: Worbblehat API Documentation
|
|
||||||
description: |-
|
|
||||||
This is the API documentation for the Worbblehat application.
|
|
||||||
It is based on the OpenAPI 3.0.3 specification.
|
|
||||||
The API is implemented using the Flask framework.
|
|
||||||
license:
|
|
||||||
name: MIT
|
|
||||||
version: 0.0.1
|
|
||||||
|
|
||||||
tags:
|
|
||||||
- name: book
|
|
||||||
description: Everything about books
|
|
||||||
- name: bookshelf
|
|
||||||
description: Operations about bookshelves
|
|
||||||
- name: loan
|
|
||||||
description: Operations about loans
|
|
||||||
|
|
||||||
paths:
|
|
||||||
/book/metadata/{isbn}:
|
|
||||||
get:
|
|
||||||
summary: Fetch a book metadata by ISBN
|
|
||||||
tags:
|
|
||||||
- book
|
|
||||||
parameters:
|
|
||||||
- name: isbn
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The ISBN of the book to retrieve
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: The book with the specified ISBN
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/BookMetadata'
|
|
||||||
'404':
|
|
||||||
description: Book not found
|
|
||||||
|
|
||||||
/book/{isbn}:
|
|
||||||
get:
|
|
||||||
summary: Fetch a book from database by ISBN
|
|
||||||
tags:
|
|
||||||
- book
|
|
||||||
parameters:
|
|
||||||
- name: isbn
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The ISBN of the book to retrieve
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: The book with the specified ISBN
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Book'
|
|
||||||
'404':
|
|
||||||
description: Book not found
|
|
||||||
post:
|
|
||||||
summary: Add a new book to the database
|
|
||||||
tags:
|
|
||||||
- book
|
|
||||||
requestBody:
|
|
||||||
description: The book to add
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Book'
|
|
||||||
responses:
|
|
||||||
'201':
|
|
||||||
description: The book was successfully added
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Book'
|
|
||||||
'400':
|
|
||||||
description: Invalid input
|
|
||||||
|
|
||||||
/book/move/{from_shelf_name}/{to_shelf_name}:
|
|
||||||
post:
|
|
||||||
summary: Move a book from one bookshelf to another
|
|
||||||
tags:
|
|
||||||
- book
|
|
||||||
- bookshelf
|
|
||||||
parameters:
|
|
||||||
- name: from_shelf_name
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The name of the bookshelf to move the book from
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: to_shelf_name
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The name of the bookshelf to move the book to
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
requestBody:
|
|
||||||
description: The book to move
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Book'
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: The book was successfully moved
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Book'
|
|
||||||
'400':
|
|
||||||
description: Invalid input
|
|
||||||
'404':
|
|
||||||
description: Bookshelf not found
|
|
||||||
|
|
||||||
/bookshelf:
|
|
||||||
get:
|
|
||||||
summary: Fetch all bookshelves from database
|
|
||||||
tags:
|
|
||||||
- bookshelf
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: All bookshelves
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/BookcaseShelf'
|
|
||||||
|
|
||||||
/bookshelf/{shelf_name}:
|
|
||||||
get:
|
|
||||||
summary: Fetch a bookshelf from database by name
|
|
||||||
tags:
|
|
||||||
- bookshelf
|
|
||||||
parameters:
|
|
||||||
- name: shelf_name
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The name of the bookshelf to retrieve
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: The bookshelf with the specified name
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/BookcaseShelf'
|
|
||||||
'404':
|
|
||||||
description: Bookshelf not found
|
|
||||||
post:
|
|
||||||
summary: Add a new bookshelf to the database
|
|
||||||
tags:
|
|
||||||
- bookshelf
|
|
||||||
requestBody:
|
|
||||||
description: The bookshelf to add
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/BookcaseShelf'
|
|
||||||
responses:
|
|
||||||
'201':
|
|
||||||
description: The bookshelf was successfully added
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/BookcaseShelf'
|
|
||||||
'400':
|
|
||||||
description: Invalid input
|
|
||||||
|
|
||||||
/bookshelf/books/{shelf_name}:
|
|
||||||
get:
|
|
||||||
summary: Fetch all books from a bookshelf
|
|
||||||
tags:
|
|
||||||
- bookshelf
|
|
||||||
parameters:
|
|
||||||
- name: shelf_name
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The name of the bookshelf to retrieve
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: The books on the specified bookshelf
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Book'
|
|
||||||
'404':
|
|
||||||
description: Bookshelf not found
|
|
||||||
|
|
||||||
/loan/{isbn}:
|
|
||||||
post:
|
|
||||||
summary: Borrow a book
|
|
||||||
tags:
|
|
||||||
- loan
|
|
||||||
parameters:
|
|
||||||
- name: isbn
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The ISBN of the book to borrow
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
requestBody:
|
|
||||||
description: The user who wants to borrow the book
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: The username of the user who wants to borrow the book
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
description: The email of the user who wants to borrow the book
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: The book was successfully borrowed
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
loan_id:
|
|
||||||
type: integer
|
|
||||||
description: The id of the loan
|
|
||||||
book:
|
|
||||||
$ref: '#/components/schemas/Book'
|
|
||||||
user:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: The username of the user who borrowed the book
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
description: The email of the user who borrowed the book
|
|
||||||
'400':
|
|
||||||
description: Invalid input
|
|
||||||
'404':
|
|
||||||
description: Book not found
|
|
||||||
'409':
|
|
||||||
description: Book already borrowed
|
|
||||||
delete:
|
|
||||||
summary: Return a book
|
|
||||||
tags:
|
|
||||||
- loan
|
|
||||||
parameters:
|
|
||||||
- name: isbn
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The ISBN of the book to return
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
requestBody:
|
|
||||||
description: The user who wants to return the book
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: The username of the user who wants to return the book
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
description: The email of the user who wants to return the book
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: The book was successfully returned
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
loan_id:
|
|
||||||
type: integer
|
|
||||||
description: The id of the loan
|
|
||||||
book:
|
|
||||||
$ref: '#/components/schemas/Book'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
components:
|
|
||||||
schemas:
|
|
||||||
BookMetadata:
|
|
||||||
description: metadata of a book
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
isbn:
|
|
||||||
type: string
|
|
||||||
description: The ISBN of the book.
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
description: The title of the book.
|
|
||||||
authors:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
description: A set of authors names of the book.
|
|
||||||
language:
|
|
||||||
type: string
|
|
||||||
description: The language of the book.
|
|
||||||
publish_date:
|
|
||||||
type: integer
|
|
||||||
description: The publish date of the book.
|
|
||||||
num_pages:
|
|
||||||
type: integer
|
|
||||||
description: The number of pages in the book.
|
|
||||||
subjects:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
description: A set of subjects related to the book.
|
|
||||||
|
|
||||||
Author:
|
|
||||||
description: An author of a book
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
description: The name of the author.
|
|
||||||
Category:
|
|
||||||
description: A category of a book
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
description: The name of the category.
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
description: The description of the category.
|
|
||||||
Language:
|
|
||||||
description: A language of a book
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
description: The name of the language.
|
|
||||||
iso639_1_code:
|
|
||||||
type: string
|
|
||||||
description: The iso639-1 code of the language.
|
|
||||||
|
|
||||||
MediaType:
|
|
||||||
description: A media type of a book
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
description: The name of the media type.
|
|
||||||
|
|
||||||
Book:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
uid:
|
|
||||||
type: integer
|
|
||||||
format: int64
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
isbn:
|
|
||||||
type: integer
|
|
||||||
owner:
|
|
||||||
type: string
|
|
||||||
amount:
|
|
||||||
type: integer
|
|
||||||
media_type:
|
|
||||||
$ref: '#/components/schemas/MediaType'
|
|
||||||
shelf:
|
|
||||||
$ref: '#/components/schemas/BookcaseShelf'
|
|
||||||
language:
|
|
||||||
$ref: '#/components/schemas/Language'
|
|
||||||
categories:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Category'
|
|
||||||
authors:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/Author'
|
|
||||||
required:
|
|
||||||
- uid
|
|
||||||
- name
|
|
||||||
- isbn
|
|
||||||
- owner
|
|
||||||
- amount
|
|
||||||
|
|
||||||
BookcaseShelf:
|
|
||||||
description: A bookshelf, its location
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
description: The name of the bookshelf.
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
description: The description of the bookshelf. Like its location.
|
|
||||||
|
|
||||||
# not sure if we keep this
|
|
||||||
User:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: The username of the user.
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
description: The email of the user.
|
|
||||||
role:
|
|
||||||
type: string
|
|
||||||
description: The role of the user.
|
|
||||||
loans:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: integer
|
|
||||||
description: The loan id's of the user.
|
|
||||||
|
|
||||||
#maybe auth.
|
|
|
@ -31,7 +31,6 @@ def create_app(args: dict[str, any] | None = None):
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def configure_admin(app):
|
def configure_admin(app):
|
||||||
admin = Admin(app, name='Worblehat', template_mode='bootstrap3')
|
admin = Admin(app, name='Worblehat', template_mode='bootstrap3')
|
||||||
admin.add_view(ModelView(Author, db.session))
|
admin.add_view(ModelView(Author, db.session))
|
||||||
|
|
|
@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
|
||||||
from .services import (
|
from .services import (
|
||||||
Config,
|
Config,
|
||||||
arg_parser,
|
arg_parser,
|
||||||
|
devscripts_arg_parser,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .deadline_daemon import DeadlineDaemon
|
from .deadline_daemon import DeadlineDaemon
|
||||||
|
@ -60,6 +61,19 @@ def main():
|
||||||
WorblehatCli.run_with_safe_exit_wrapper(sql_session)
|
WorblehatCli.run_with_safe_exit_wrapper(sql_session)
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
if args.command == 'devscripts':
|
||||||
|
sql_session = _connect_to_database(echo=Config['logging.debug_sql'])
|
||||||
|
if args.script == 'seed-content-for-deadline-daemon':
|
||||||
|
from .devscripts.seed_content_for_deadline_daemon import main
|
||||||
|
main(sql_session)
|
||||||
|
elif args.script == 'seed-test-data':
|
||||||
|
from .devscripts.seed_test_data import main
|
||||||
|
main(sql_session)
|
||||||
|
else:
|
||||||
|
print(devscripts_arg_parser.format_help())
|
||||||
|
exit(1)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
if args.command == 'flask-dev':
|
if args.command == 'flask-dev':
|
||||||
flask_dev_main()
|
flask_dev_main()
|
||||||
exit(0)
|
exit(0)
|
||||||
|
@ -71,3 +85,4 @@ def main():
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
print(arg_parser.format_help())
|
print(arg_parser.format_help())
|
||||||
|
exit(1)
|
|
@ -2,10 +2,11 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
|
ForeignKey,
|
||||||
Integer,
|
Integer,
|
||||||
SmallInteger,
|
SmallInteger,
|
||||||
String,
|
String,
|
||||||
ForeignKey,
|
Text,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
|
@ -31,13 +32,16 @@ if TYPE_CHECKING:
|
||||||
from .Language import Language
|
from .Language import Language
|
||||||
from .MediaType import MediaType
|
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)
|
isbn: Mapped[int] = mapped_column(String, unique=True, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(Text, index=True)
|
||||||
owner: Mapped[str] = mapped_column(String, default='PVV')
|
owner: Mapped[str] = mapped_column(String, default='PVV')
|
||||||
amount: Mapped[int] = mapped_column(SmallInteger, default=1)
|
amount: Mapped[int] = mapped_column(SmallInteger, default=1)
|
||||||
|
|
||||||
fk_media_type_uid: Mapped[int] = mapped_column(ForeignKey('MediaType.uid'))
|
fk_media_type_uid: Mapped[int] = mapped_column(ForeignKey('MediaType.uid'))
|
||||||
fk_bookcase_shelf_uid: Mapped[int | None] = mapped_column(ForeignKey('BookcaseShelf.uid'))
|
fk_bookcase_shelf_uid: Mapped[int] = mapped_column(ForeignKey('BookcaseShelf.uid'))
|
||||||
fk_language_uid: Mapped[int | None] = mapped_column(ForeignKey('Language.uid'))
|
fk_language_uid: Mapped[int | None] = mapped_column(ForeignKey('Language.uid'))
|
||||||
|
|
||||||
media_type: Mapped[MediaType] = relationship(back_populates='items')
|
media_type: Mapped[MediaType] = relationship(back_populates='items')
|
||||||
|
@ -64,3 +68,12 @@ class BookcaseItem(Base, UidMixin, UniqueNameMixin):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.isbn = isbn
|
self.isbn = isbn
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_isbn(cls, isbn: str, sql_session: Session = db.session) -> Self | None:
|
||||||
|
"""
|
||||||
|
NOTE:
|
||||||
|
This method defaults to using the flask_sqlalchemy session.
|
||||||
|
It will not work outside of a request context, unless another session is provided.
|
||||||
|
"""
|
||||||
|
return sql_session.query(cls).where(cls.isbn == isbn).one_or_none()
|
|
@ -21,7 +21,8 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class BookcaseItemBorrowingQueue(Base, UidMixin):
|
class BookcaseItemBorrowingQueue(Base, UidMixin):
|
||||||
username: Mapped[str] = mapped_column(String)
|
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)
|
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)
|
||||||
|
|
|
@ -21,3 +21,7 @@ class DeadlineDaemonLastRunDatetime(Base):
|
||||||
)
|
)
|
||||||
uid: Mapped[bool] = mapped_column(Boolean, primary_key=True, default=True)
|
uid: Mapped[bool] = mapped_column(Boolean, primary_key=True, default=True)
|
||||||
time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
|
||||||
|
|
||||||
|
def __init__(self, time: datetime | None = None):
|
||||||
|
if time is not None:
|
||||||
|
self.time = time
|
|
@ -1,8 +1,8 @@
|
||||||
"""initial_migration
|
"""initial_migration
|
||||||
|
|
||||||
Revision ID: d51c7172d2f2
|
Revision ID: 7dfbf8a8dec8
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2023-05-06 17:46:39.230122
|
Create Date: 2024-07-31 21:07:13.434012
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'd51c7172d2f2'
|
revision = '7dfbf8a8dec8'
|
||||||
down_revision = None
|
down_revision = None
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
@ -44,6 +44,12 @@ def upgrade() -> None:
|
||||||
with op.batch_alter_table('Category', schema=None) as batch_op:
|
with op.batch_alter_table('Category', schema=None) as batch_op:
|
||||||
batch_op.create_index(batch_op.f('ix_Category_name'), ['name'], unique=True)
|
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',
|
op.create_table('Language',
|
||||||
sa.Column('iso639_1_code', sa.String(length=2), nullable=False),
|
sa.Column('iso639_1_code', sa.String(length=2), nullable=False),
|
||||||
sa.Column('uid', sa.Integer(), nullable=False),
|
sa.Column('uid', sa.Integer(), nullable=False),
|
||||||
|
@ -75,13 +81,13 @@ def upgrade() -> None:
|
||||||
)
|
)
|
||||||
op.create_table('BookcaseItem',
|
op.create_table('BookcaseItem',
|
||||||
sa.Column('isbn', sa.String(), nullable=False),
|
sa.Column('isbn', sa.String(), nullable=False),
|
||||||
|
sa.Column('name', sa.Text(), nullable=False),
|
||||||
sa.Column('owner', sa.String(), nullable=False),
|
sa.Column('owner', sa.String(), nullable=False),
|
||||||
sa.Column('amount', sa.SmallInteger(), nullable=False),
|
sa.Column('amount', sa.SmallInteger(), nullable=False),
|
||||||
sa.Column('fk_media_type_uid', sa.Integer(), 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_bookcase_shelf_uid', sa.Integer(), nullable=False),
|
||||||
sa.Column('fk_language_uid', sa.Integer(), nullable=True),
|
sa.Column('fk_language_uid', sa.Integer(), nullable=True),
|
||||||
sa.Column('uid', sa.Integer(), nullable=False),
|
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_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_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.ForeignKeyConstraint(['fk_media_type_uid'], ['MediaType.uid'], name=op.f('fk_BookcaseItem_fk_media_type_uid_MediaType')),
|
||||||
|
@ -89,13 +95,13 @@ def upgrade() -> None:
|
||||||
)
|
)
|
||||||
with op.batch_alter_table('BookcaseItem', schema=None) as batch_op:
|
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_isbn'), ['isbn'], unique=True)
|
||||||
batch_op.create_index(batch_op.f('ix_BookcaseItem_name'), ['name'], unique=True)
|
batch_op.create_index(batch_op.f('ix_BookcaseItem_name'), ['name'], unique=False)
|
||||||
|
|
||||||
op.create_table('BookcaseItemBorrowing',
|
op.create_table('BookcaseItemBorrowing',
|
||||||
sa.Column('username', sa.String(), nullable=False),
|
sa.Column('username', sa.String(), nullable=False),
|
||||||
sa.Column('start_time', sa.DateTime(), nullable=False),
|
sa.Column('start_time', sa.DateTime(), nullable=False),
|
||||||
sa.Column('end_time', sa.DateTime(), nullable=False),
|
sa.Column('end_time', sa.DateTime(), nullable=False),
|
||||||
sa.Column('delivered', sa.Boolean(), nullable=False),
|
sa.Column('delivered', sa.DateTime(), nullable=True),
|
||||||
sa.Column('fk_bookcase_item_uid', sa.Integer(), nullable=False),
|
sa.Column('fk_bookcase_item_uid', sa.Integer(), nullable=False),
|
||||||
sa.Column('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.ForeignKeyConstraint(['fk_bookcase_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_BookcaseItemBorrowing_fk_bookcase_item_uid_BookcaseItem')),
|
||||||
|
@ -106,8 +112,9 @@ def upgrade() -> None:
|
||||||
|
|
||||||
op.create_table('BookcaseItemBorrowingQueue',
|
op.create_table('BookcaseItemBorrowingQueue',
|
||||||
sa.Column('username', sa.String(), nullable=False),
|
sa.Column('username', sa.String(), nullable=False),
|
||||||
sa.Column('entered_queue_time', sa.DateTime(), nullable=True),
|
sa.Column('entered_queue_time', sa.DateTime(), nullable=False),
|
||||||
sa.Column('should_notify_user', sa.Boolean(), nullable=True),
|
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('fk_bookcase_item_uid', sa.Integer(), nullable=False),
|
||||||
sa.Column('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.ForeignKeyConstraint(['fk_bookcase_item_uid'], ['BookcaseItem.uid'], name=op.f('fk_BookcaseItemBorrowingQueue_fk_bookcase_item_uid_BookcaseItem')),
|
||||||
|
@ -160,6 +167,7 @@ def downgrade() -> None:
|
||||||
batch_op.drop_index(batch_op.f('ix_Language_iso639_1_code'))
|
batch_op.drop_index(batch_op.f('ix_Language_iso639_1_code'))
|
||||||
|
|
||||||
op.drop_table('Language')
|
op.drop_table('Language')
|
||||||
|
op.drop_table('DeadlineDaemonLastRunDatetime')
|
||||||
with op.batch_alter_table('Category', schema=None) as batch_op:
|
with op.batch_alter_table('Category', schema=None) as batch_op:
|
||||||
batch_op.drop_index(batch_op.f('ix_Category_name'))
|
batch_op.drop_index(batch_op.f('ix_Category_name'))
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
from .argument_parser import arg_parser
|
from .argument_parser import (
|
||||||
|
arg_parser,
|
||||||
|
devscripts_arg_parser,
|
||||||
|
)
|
||||||
from .bookcase_item import (
|
from .bookcase_item import (
|
||||||
create_bookcase_item_from_isbn,
|
create_bookcase_item_from_isbn,
|
||||||
is_valid_isbn,
|
is_valid_isbn,
|
||||||
|
|
|
@ -31,6 +31,19 @@ subparsers.add_parser(
|
||||||
help = 'Start the web interface in production mode',
|
help = 'Start the web interface in production mode',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
devscripts_arg_parser = subparsers.add_parser('devscripts', help='Run development scripts')
|
||||||
|
devscripts_subparsers = devscripts_arg_parser.add_subparsers(dest='script')
|
||||||
|
|
||||||
|
devscripts_subparsers.add_parser(
|
||||||
|
'seed-test-data',
|
||||||
|
help = 'Seed test data in the database',
|
||||||
|
)
|
||||||
|
|
||||||
|
devscripts_subparsers.add_parser(
|
||||||
|
'seed-content-for-deadline-daemon',
|
||||||
|
help = 'Seed data tailorded for testing the deadline daemon, into the database',
|
||||||
|
)
|
||||||
|
|
||||||
arg_parser.add_argument(
|
arg_parser.add_argument(
|
||||||
'-V',
|
'-V',
|
||||||
'--version',
|
'--version',
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import isbnlib
|
import isbnlib
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.book_metadata_fetcher import fetcher_dict
|
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from worblehat.models import (
|
from .metadata_fetchers import fetch_metadata_from_multiple_sources
|
||||||
|
from ..models import (
|
||||||
Author,
|
Author,
|
||||||
BookcaseItem,
|
BookcaseItem,
|
||||||
Language,
|
Language,
|
||||||
|
@ -27,32 +26,35 @@ def is_valid_isbn(isbn: str) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def create_bookcase_item_from_isbn(isbn: str, sql_session: Session) -> BookcaseItem | None:
|
def create_bookcase_item_from_isbn(isbn: str, sql_session: Session) -> BookcaseItem | None:
|
||||||
# metadata = isbnlib.meta(isbn, 'openl')
|
"""
|
||||||
|
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.
|
||||||
|
|
||||||
metadata = fetcher_dict(isbn)
|
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.
|
||||||
if len(metadata.keys()) == 0:
|
"""
|
||||||
|
metadata = fetch_metadata_from_multiple_sources(isbn)
|
||||||
|
if len(metadata) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
metadata = metadata[0]
|
||||||
|
|
||||||
bookcase_item = BookcaseItem(
|
bookcase_item = BookcaseItem(
|
||||||
name = metadata.get('Title'),
|
name = metadata.title,
|
||||||
isbn = int(isbn.replace('-', '')),
|
isbn = int(isbn.replace('-', '')),
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(authors := metadata.get('Authors')) > 0:
|
if len(authors := metadata.authors) > 0:
|
||||||
for author in authors:
|
for author in authors:
|
||||||
bookcase_item.authors.add(Author(author))
|
bookcase_item.authors.add(Author(author))
|
||||||
|
|
||||||
if (language := metadata.get('Language')):
|
if (language := metadata.language):
|
||||||
bookcase_item.language = sql_session.scalars(
|
bookcase_item.language = sql_session.scalars(
|
||||||
select(Language)
|
select(Language)
|
||||||
.where(Language.iso639_1_code == language)
|
.where(Language.iso639_1_code == language)
|
||||||
).one()
|
).one()
|
||||||
|
|
||||||
return bookcase_item
|
return bookcase_item
|
||||||
|
|
||||||
|
|
||||||
# if __name__ == '__main__':
|
|
||||||
# item = create_bookcase_item_from_isbn('9780593678510', None)
|
|
||||||
# print(item)
|
|
|
@ -24,6 +24,9 @@ class Config:
|
||||||
]
|
]
|
||||||
|
|
||||||
def __class_getitem__(cls, name: str) -> Any:
|
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
|
__config = cls._config
|
||||||
for attr in name.split('.'):
|
for attr in name.split('.'):
|
||||||
__config = __config.get(attr)
|
__config = __config.get(attr)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from pathlib import Path
|
|
||||||
import smtplib
|
import smtplib
|
||||||
|
|
||||||
|
from textwrap import indent
|
||||||
|
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
|
||||||
|
@ -8,7 +9,6 @@ from .config import Config
|
||||||
|
|
||||||
|
|
||||||
def send_email(to: str, subject: str, body: str):
|
def send_email(to: str, subject: str, body: str):
|
||||||
if Config['smtp.enabled']:
|
|
||||||
msg = MIMEMultipart()
|
msg = MIMEMultipart()
|
||||||
msg['From'] = Config['smtp.from']
|
msg['From'] = Config['smtp.from']
|
||||||
msg['To'] = to
|
msg['To'] = to
|
||||||
|
@ -18,6 +18,7 @@ def send_email(to: str, subject: str, body: str):
|
||||||
msg['Subject'] = subject
|
msg['Subject'] = subject
|
||||||
msg.attach(MIMEText(body, 'plain'))
|
msg.attach(MIMEText(body, 'plain'))
|
||||||
|
|
||||||
|
if Config['smtp.enabled'] and not Config['deadline_daemon.dryrun']:
|
||||||
try:
|
try:
|
||||||
with smtplib.SMTP(Config['smtp.host'], Config['smtp.port']) as server:
|
with smtplib.SMTP(Config['smtp.host'], Config['smtp.port']) as server:
|
||||||
server.starttls()
|
server.starttls()
|
||||||
|
@ -31,6 +32,4 @@ def send_email(to: str, subject: str, body: str):
|
||||||
print(err)
|
print(err)
|
||||||
else:
|
else:
|
||||||
print('Debug: Email sending is disabled, so the following email was not sent:')
|
print('Debug: Email sending is disabled, so the following email was not sent:')
|
||||||
print(f' To: {to}')
|
print(indent(msg.as_string(), ' '))
|
||||||
print(f' Subject: {subject}')
|
|
||||||
print(f' Body: {body}')
|
|
|
@ -1,42 +1,62 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Set
|
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
|
@dataclass
|
||||||
class BookMetadata:
|
class BookMetadata:
|
||||||
"""
|
"""A class representing metadata for a book."""
|
||||||
A class representing the metadata for a book.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
isbn (str): The ISBN of the book.
|
|
||||||
title (str): The title of the book.
|
|
||||||
authors (Set[str]): A set of authors of the book.
|
|
||||||
language (str): The language of the book.
|
|
||||||
publish_date (int): The publish date of the book.
|
|
||||||
num_pages (int): The number of pages in the book.
|
|
||||||
subjects (Set[str]): A set of subjects related to the book.
|
|
||||||
"""
|
|
||||||
isbn: str
|
isbn: str
|
||||||
title: str
|
title: str
|
||||||
|
# The source of the metadata provider
|
||||||
|
source: str
|
||||||
authors: Set[str]
|
authors: Set[str]
|
||||||
language: str
|
language: str | None
|
||||||
publish_date: int
|
publish_date: str | None
|
||||||
num_pages: int
|
num_pages: int | None
|
||||||
subjects: Set[str]
|
subjects: Set[str]
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict[str, any]:
|
||||||
|
return {
|
||||||
dict = {
|
'isbn': self.isbn,
|
||||||
'Isbn': self.isbn,
|
'title': self.title,
|
||||||
'Title': self.title,
|
'source': self.metadata_source_id(),
|
||||||
'Authors': None,
|
'authors': set() if self.authors is None else self.authors,
|
||||||
'Language': self.language,
|
'language': self.language,
|
||||||
'Publish_date': self.publish_date,
|
'publish_date': self.publish_date,
|
||||||
'Num_pages': self.num_pages,
|
'num_pages': self.num_pages,
|
||||||
'Subjects': None
|
'subjects': set() if self.subjects is None else self.subjects
|
||||||
}
|
}
|
||||||
if self.authors is not None:
|
|
||||||
dict['Authors'] = self.authors
|
|
||||||
if self.subjects is not None:
|
|
||||||
dict['Subjects'] = self.subjects
|
|
||||||
|
|
||||||
return dict
|
def validate(self) -> None:
|
||||||
|
if not self.isbn:
|
||||||
|
raise ValueError('Missing ISBN')
|
||||||
|
if not self.title:
|
||||||
|
raise ValueError('Missing title')
|
||||||
|
if not self.source:
|
||||||
|
raise ValueError('Missing source')
|
||||||
|
if not self.authors:
|
||||||
|
raise ValueError('Missing authors')
|
||||||
|
|
||||||
|
if self.language is not None and self.language not in LANGUAGES:
|
||||||
|
raise ValueError(f'Invalid language: {self.language}. Consider adding it to the LANGUAGES set if you think this is a mistake.')
|
||||||
|
|
||||||
|
if self.num_pages is not None and self.num_pages < 0:
|
||||||
|
raise ValueError(f'Invalid number of pages: {self.num_pages}')
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
#base fetcher.
|
#base fetcher.
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Optional
|
from .BookMetadata import BookMetadata
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
|
|
||||||
class BookMetadataFetcher(ABC):
|
class BookMetadataFetcher(ABC):
|
||||||
__metadata: BookMetadata
|
"""
|
||||||
|
A base class for metadata fetchers.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, isbn: Optional[str] = None):
|
@classmethod
|
||||||
if isbn:
|
|
||||||
self.fetch_metadata(isbn)
|
|
||||||
else:
|
|
||||||
self.__metadata = None
|
|
||||||
|
|
||||||
#fetches metadata from the isbn and sets the attributes of the class
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def fetch_metadata(self, isbn: str) -> BookMetadata:
|
def metadata_source_id(cls) -> str:
|
||||||
return self.__metadata
|
"""Returns a unique identifier for the metadata source, to identify where the metadata came from."""
|
||||||
|
pass
|
||||||
|
|
||||||
def get_metadata(self) -> BookMetadata:
|
@classmethod
|
||||||
return self.__metadata
|
@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(
|
||||||
|
f"https://www.googleapis.com/books/v1/volumes",
|
||||||
|
params = {"q": f"isbn:{isbn}"},
|
||||||
|
).json()
|
||||||
|
data = jsonInput.get("items")[0].get("volumeInfo")
|
||||||
|
|
||||||
|
authors = set(data.get("authors") or [])
|
||||||
|
title = data.get("title")
|
||||||
|
publishDate = data.get("publish_date")
|
||||||
|
numberOfPages = data.get("number_of_pages")
|
||||||
|
if numberOfPages:
|
||||||
|
numberOfPages = int(numberOfPages)
|
||||||
|
subjects = set(data.get("categories") or [])
|
||||||
|
languages = data.get("languages")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return BookMetadata(
|
||||||
|
isbn = isbn,
|
||||||
|
title = title,
|
||||||
|
source = cls.metadata_source_id(),
|
||||||
|
authors = authors,
|
||||||
|
language = languages,
|
||||||
|
publish_date = publishDate,
|
||||||
|
num_pages = numberOfPages,
|
||||||
|
subjects = subjects,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
book_data = GoogleBooksFetcher.fetch_metadata('0132624788')
|
||||||
|
book_data.validate()
|
||||||
|
print(book_data)
|
|
@ -1,98 +0,0 @@
|
||||||
"""
|
|
||||||
This module provides a class for fetching book metadata from Google Books API.
|
|
||||||
|
|
||||||
The class `GoogleBooksFetcher` implements the `BookMetadataFetcher` interface and provides a method `fetch_metadata`
|
|
||||||
that takes an ISBN as input and returns a `BookMetadata` object containing the metadata for the book with the given ISBN.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
To fetch metadata for a book with ISBN '0132624788', create an instance of `GoogleBooksFetcher` and call its
|
|
||||||
`fetch_metadata` method:
|
|
||||||
|
|
||||||
>>> fetcher = GoogleBooksFetcher()
|
|
||||||
>>> book_data = fetcher.fetch_metadata('0132624788')
|
|
||||||
>>> print(book_data)
|
|
||||||
BookMetadata(isbn='0132624788', title='Elements of the theory of computation', authors=['Harry R. Lewis'], language='English', publish_date=1998, num_pages=361, subjects=['Machine theory.', 'Formal languages.', 'Computational complexity.', 'Logic, Symbolic and mathematical.'])
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Dict, Optional
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
|
|
||||||
class GoogleBooksFetcher(BookMetadataFetcher):
|
|
||||||
"""
|
|
||||||
A class for fetching book metadata from Google Books API.
|
|
||||||
|
|
||||||
This class implements the `BookMetadataFetcher` interface and provides a method `fetch_metadata`
|
|
||||||
that takes an ISBN as input and returns a `BookMetadata` object containing the metadata for the book with the given ISBN.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def fetch_metadata(self, isbn: str) -> BookMetadata:
|
|
||||||
"""
|
|
||||||
Fetches book metadata for the given ISBN from Google Books API.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
isbn (str): The ISBN of the book to fetch metadata for.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
BookMetadata: A `BookMetadata` object containing the metadata for the book with the given ISBN.
|
|
||||||
Returns `None` if no metadata is found for the given ISBN.
|
|
||||||
"""
|
|
||||||
metadata = self.__googlebooks__(isbn)
|
|
||||||
if not metadata or len(metadata.keys()) == 0 or (not metadata.get('Title') and not metadata.get('Authors') and not metadata.get('Language')):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# parse the metadata into a BookMetadata object
|
|
||||||
self.__metadata__ = BookMetadata(
|
|
||||||
isbn = isbn,
|
|
||||||
title = metadata.get('Title'),
|
|
||||||
authors = metadata.get('Authors'),
|
|
||||||
language = metadata.get('Language'),
|
|
||||||
publish_date = metadata.get('PublishDate'),
|
|
||||||
num_pages = metadata.get('NumberOfPages'),
|
|
||||||
subjects = metadata.get('Subjects'),
|
|
||||||
)
|
|
||||||
return self.__metadata__
|
|
||||||
|
|
||||||
def __googlebooks__(self, isbn):
|
|
||||||
"""
|
|
||||||
Internal method do not use.
|
|
||||||
Fetches book metadata for the given ISBN from Google Books API.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
jsonInput = json.loads(requests.get("https://openlibrary.org/isbn/"+str(isbn)+".json").text)
|
|
||||||
#format data to standard format.
|
|
||||||
authors = jsonInput.get("authors")
|
|
||||||
for i in range(len(authors)):
|
|
||||||
authors[i] = json.loads(requests.get("https://openlibrary.org"+str(authors[i].get("key"))+".json").text).get("name")
|
|
||||||
authors = list(set(authors))
|
|
||||||
|
|
||||||
title = jsonInput.get("title")
|
|
||||||
publishDate = jsonInput.get("publish_date")
|
|
||||||
numberOfPages = jsonInput.get("number_of_pages")
|
|
||||||
genre = jsonInput.get("subjects")
|
|
||||||
language = jsonInput.get("languages")[0]
|
|
||||||
language = json.loads(requests.get("https://openlibrary.org"+language.get("key")+".json").text).get("name")
|
|
||||||
subjects = jsonInput.get("subjects")
|
|
||||||
|
|
||||||
#create a dictionary to represent a book and its data
|
|
||||||
bookData = {
|
|
||||||
"Authors": authors,
|
|
||||||
"Title": title,
|
|
||||||
"PublishDate": int(publishDate),
|
|
||||||
"NumberOfPages": numberOfPages,
|
|
||||||
"Genre": genre,
|
|
||||||
"Language": language,
|
|
||||||
"Subjects": subjects
|
|
||||||
}
|
|
||||||
return bookData
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
fetcher = GoogleBooksFetcher()
|
|
||||||
book_data = fetcher.fetch_metadata('0132624788')
|
|
||||||
print(book_data)
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
"""
|
||||||
|
A BookMetadataFetcher for the Open Library API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
||||||
|
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
||||||
|
|
||||||
|
LANGUAGE_MAP = {
|
||||||
|
"Norwegian": "no",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OpenLibraryFetcher(BookMetadataFetcher):
|
||||||
|
@classmethod
|
||||||
|
def metadata_source_id(_cls) -> str:
|
||||||
|
return "open_library"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
||||||
|
try:
|
||||||
|
jsonInput = requests.get(f"https://openlibrary.org/isbn/{isbn}.json").json()
|
||||||
|
|
||||||
|
author_keys = jsonInput.get("authors") or []
|
||||||
|
author_names = set()
|
||||||
|
for author_key in author_keys:
|
||||||
|
key = author_key.get('key')
|
||||||
|
author_name = requests.get(f"https://openlibrary.org/{key}.json").json().get("name")
|
||||||
|
author_names.add(author_name)
|
||||||
|
|
||||||
|
title = jsonInput.get("title")
|
||||||
|
publishDate = jsonInput.get("publish_date")
|
||||||
|
|
||||||
|
numberOfPages = jsonInput.get("number_of_pages")
|
||||||
|
if numberOfPages:
|
||||||
|
numberOfPages = int(numberOfPages)
|
||||||
|
|
||||||
|
language_key = jsonInput.get("languages")[0].get("key")
|
||||||
|
language = requests.get(f"https://openlibrary.org/{language_key}.json").json().get("identifiers").get("iso_639_1")[0]
|
||||||
|
subjects = set(jsonInput.get("subjects") or [])
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return BookMetadata(
|
||||||
|
isbn = isbn,
|
||||||
|
title = title,
|
||||||
|
source = cls.metadata_source_id(),
|
||||||
|
authors = author_names,
|
||||||
|
language = language,
|
||||||
|
publish_date = publishDate,
|
||||||
|
num_pages = numberOfPages,
|
||||||
|
subjects = subjects,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
book_data = OpenLibraryFetcher.fetch_metadata('9788205530751')
|
||||||
|
book_data.validate()
|
||||||
|
print(book_data)
|
|
@ -1,68 +0,0 @@
|
||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
# import isbnlib
|
|
||||||
#used these instead of isbnlib as i have already written the code for them
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
|
|
||||||
class OpenLibraryFetcher(BookMetadataFetcher):
|
|
||||||
|
|
||||||
def fetch_metadata(self, isbn: str) -> BookMetadata:
|
|
||||||
metadata = self.__openLibrary(isbn)
|
|
||||||
if not metadata or len(metadata.keys()) == 0 or (not metadata.get('Title') and not metadata.get('Authors') and not metadata.get('Language')):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# parse the metadata into a BookMetadata object
|
|
||||||
self.__metadata = BookMetadata(
|
|
||||||
isbn = isbn,
|
|
||||||
title = metadata.get('Title'),
|
|
||||||
authors = metadata.get('Authors'),
|
|
||||||
language = metadata.get('Language'),
|
|
||||||
publish_date = metadata.get('PublishDate'),
|
|
||||||
num_pages = metadata.get('NumberOfPages'),
|
|
||||||
subjects = metadata.get('Subjects'),
|
|
||||||
)
|
|
||||||
return self.__metadata
|
|
||||||
|
|
||||||
|
|
||||||
#create a dictionary to represent a book and its data
|
|
||||||
#gather data from openlibrary api and return it directly as json
|
|
||||||
def __openLibrary(self, isbn):
|
|
||||||
#get data from openlibrary
|
|
||||||
try:
|
|
||||||
jsonInput = json.loads(requests.get("https://openlibrary.org/isbn/"+str(isbn)+".json").text)
|
|
||||||
#format data to standard format.
|
|
||||||
authors = jsonInput.get("authors")
|
|
||||||
for i in range(len(authors)):
|
|
||||||
authors[i] = json.loads(requests.get("https://openlibrary.org"+str(authors[i].get("key"))+".json").text).get("name")
|
|
||||||
authors = list(set(authors))
|
|
||||||
|
|
||||||
title = jsonInput.get("title")
|
|
||||||
publishDate = jsonInput.get("publish_date")
|
|
||||||
numberOfPages = jsonInput.get("number_of_pages")
|
|
||||||
genre = jsonInput.get("subjects")
|
|
||||||
language = jsonInput.get("languages")[0]
|
|
||||||
language = json.loads(requests.get("https://openlibrary.org"+language.get("key")+".json").text).get("name")
|
|
||||||
subjects = jsonInput.get("subjects")
|
|
||||||
|
|
||||||
#create a dictionary to represent a book and its data
|
|
||||||
bookData = {
|
|
||||||
"Authors": authors,
|
|
||||||
"Title": title,
|
|
||||||
"PublishDate": int(publishDate),
|
|
||||||
"NumberOfPages": numberOfPages,
|
|
||||||
"Genre": genre,
|
|
||||||
"Language": language,
|
|
||||||
"Subjects": subjects
|
|
||||||
}
|
|
||||||
return bookData
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
fetcher = OpenLibraryFetcher()
|
|
||||||
book_data = fetcher.fetch_metadata('0132624788')
|
|
||||||
print(book_data)
|
|
|
@ -1,88 +1,109 @@
|
||||||
from typing import Dict, Optional, List
|
"""
|
||||||
|
A BookMetadataFetcher that webscrapes https://outland.no/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
# import isbnlib
|
|
||||||
#used these instead of isbnlib as i have already written the code for them
|
|
||||||
import json
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
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):
|
class OutlandScraperFetcher(BookMetadataFetcher):
|
||||||
|
@classmethod
|
||||||
|
def metadata_source_id(_cls) -> str:
|
||||||
|
return "outland_scraper"
|
||||||
|
|
||||||
def fetch_metadata(self, isbn: str) -> BookMetadata:
|
@classmethod
|
||||||
metadata = self.__outland(isbn)
|
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
||||||
if not metadata or len(metadata.keys()) == 0 or (not metadata.get('Title') and not metadata.get('Authors') and not metadata.get('Language')):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# parse the metadata into a BookMetadata object
|
|
||||||
self.__metadata = BookMetadata(
|
|
||||||
isbn = isbn,
|
|
||||||
title = metadata.get('Title'),
|
|
||||||
authors = metadata.get('Authors'),
|
|
||||||
language = metadata.get('Language'),
|
|
||||||
publish_date = metadata.get('PublishDate'),
|
|
||||||
num_pages = metadata.get('NumberOfPages'),
|
|
||||||
subjects = metadata.get('Subjects'),
|
|
||||||
)
|
|
||||||
return self.__metadata
|
|
||||||
|
|
||||||
#create a dictionary to represent a book and its data
|
|
||||||
#gather data from openlibrary api and return it directly as json
|
|
||||||
def __outland(self, isbn):
|
|
||||||
#get data from openlibrary
|
|
||||||
try:
|
try:
|
||||||
|
# Find the link to the product page
|
||||||
from bs4 import BeautifulSoup
|
response = requests.get(f"https://outland.no/{isbn}")
|
||||||
url = "https://outland.no/"+isbn
|
|
||||||
response = requests.get(url)
|
|
||||||
soup = BeautifulSoup(response.content, "html.parser")
|
soup = BeautifulSoup(response.content, "html.parser")
|
||||||
#get all hrefs from elements with class "product-item-link"
|
|
||||||
soup = soup.find_all("a", class_="product-item-link")
|
soup = soup.find_all("a", class_="product-item-link")
|
||||||
#get the first href
|
|
||||||
href = soup[0].get("href")
|
href = soup[0].get("href")
|
||||||
|
|
||||||
#get data from the first href
|
# Find the metadata on the product page
|
||||||
response = requests.get(href)
|
response = requests.get(href)
|
||||||
soup = BeautifulSoup(response.content, "html.parser")
|
soup = BeautifulSoup(response.content, "html.parser")
|
||||||
#get all elements with class "language"
|
|
||||||
data = soup.find_all("td", class_="col data")
|
data = soup.find_all("td", class_="col data")
|
||||||
base = soup.find_all("span", class_="base")[0].text
|
|
||||||
releaseDate = soup.find_all("span", class_="release-date")[0].text.strip()
|
|
||||||
#only keep the year of the release date
|
|
||||||
releaseDate = releaseDate[-4:]
|
|
||||||
|
|
||||||
#get the element withch contains anything in the intestingData list anywhere in it.
|
# Collect the metadata
|
||||||
intrestingData = {
|
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",
|
"Authors": "Forfattere",
|
||||||
"NumberOfPages": "Antall Sider",
|
"NumberOfPages": "Antall Sider",
|
||||||
"Genre": "Sjanger",
|
"Genre": "Sjanger",
|
||||||
"Language": "Språk",
|
"Language": "Språk",
|
||||||
"Subjects": "Serie"
|
"Subjects": "Serie"
|
||||||
}
|
}
|
||||||
bookData = {
|
|
||||||
"Title": base,
|
|
||||||
"PublishDate": releaseDate,
|
|
||||||
"Authors": None,
|
|
||||||
"NumberOfPages": None,
|
|
||||||
"Genre": None,
|
|
||||||
"Language": None,
|
|
||||||
"Subjects": None
|
|
||||||
}
|
|
||||||
|
|
||||||
for value in data:
|
for value in data:
|
||||||
for key in intrestingData:
|
for key in dataKeyMap:
|
||||||
if str(value).lower().__contains__(intrestingData[key].lower()):
|
if str(value).lower().__contains__(dataKeyMap[key].lower()):
|
||||||
#get the next element in the list and add it to the bookData dict
|
|
||||||
bookData[key] = value.text
|
bookData[key] = value.text
|
||||||
break
|
break
|
||||||
|
|
||||||
return bookData
|
if bookData["Language"] is not None:
|
||||||
except Exception as e:
|
bookData["Language"] = LANGUAGE_MAP.get(bookData["Language"])
|
||||||
print(str(e))
|
|
||||||
return False
|
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__':
|
if __name__ == '__main__':
|
||||||
fetcher = OutlandScraperFetcher()
|
book_data = OutlandScraperFetcher.fetch_metadata('9781947808225')
|
||||||
book_data = fetcher.fetch_metadata('9781947808225')
|
book_data.validate()
|
||||||
print(book_data)
|
print(book_data)
|
|
@ -1,51 +0,0 @@
|
||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
# import isbnlib
|
|
||||||
#used these instead of isbnlib as i have already written the code for them
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
from tsg.client import SyncTSGClient
|
|
||||||
|
|
||||||
|
|
||||||
class StorygraphFetcher(BookMetadataFetcher):
|
|
||||||
|
|
||||||
def fetch_metadata(self, isbn: str) -> BookMetadata:
|
|
||||||
|
|
||||||
client = SyncTSGClient()
|
|
||||||
books = client.get_browse(text=isbn)
|
|
||||||
|
|
||||||
if len(books) == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
book = books[0]
|
|
||||||
|
|
||||||
if book is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if book.series is not None:
|
|
||||||
series = book.series.name[0]
|
|
||||||
else:
|
|
||||||
series = None
|
|
||||||
|
|
||||||
# parse the metadata into a BookMetadata object
|
|
||||||
self.__metadata = BookMetadata(
|
|
||||||
isbn = isbn,
|
|
||||||
title = book.title,
|
|
||||||
authors = book.author_names,
|
|
||||||
language = None,
|
|
||||||
publish_date = None,
|
|
||||||
num_pages = None,
|
|
||||||
subjects = series,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.__metadata
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
fetcher = StorygraphFetcher()
|
|
||||||
book_data = fetcher.fetch_metadata('9780593678510')
|
|
||||||
print(book_data)
|
|
|
@ -1,3 +1 @@
|
||||||
#empty comment
|
from .book_metadata_fetcher import fetch_metadata_from_multiple_sources
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
|
|
@ -3,118 +3,78 @@ this module contains the fetch_book_metadata() function which fetches book metad
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import threading
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
||||||
from worblehat.services.metadata_fetchers.GooglebooksFetcher import GoogleBooksFetcher
|
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
||||||
from worblehat.services.metadata_fetchers.OpenlibraryFetcher import OpenLibraryFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.StorygraphFetcher import StorygraphFetcher
|
from worblehat.services.metadata_fetchers.GoogleBooksFetcher import GoogleBooksFetcher
|
||||||
|
from worblehat.services.metadata_fetchers.OpenLibraryFetcher import OpenLibraryFetcher
|
||||||
from worblehat.services.metadata_fetchers.OutlandScraperFetcher import OutlandScraperFetcher
|
from worblehat.services.metadata_fetchers.OutlandScraperFetcher import OutlandScraperFetcher
|
||||||
#... import more fetchers here ...
|
|
||||||
|
|
||||||
def fetcher(isbn:str) -> (BookMetadata, List[BookMetadata]):
|
|
||||||
"""_summary_
|
|
||||||
This function fetches book metadata from multiple sources in threads and returns the higest ranked non-None result.
|
|
||||||
|
|
||||||
Args:
|
# The order of these fetchers determines the priority of the sources.
|
||||||
isbn (str): isbn of the book to fetch metadata for
|
# The first fetcher in the list has the highest priority.
|
||||||
|
FETCHERS: list[BookMetadataFetcher] = [
|
||||||
|
OpenLibraryFetcher,
|
||||||
|
GoogleBooksFetcher,
|
||||||
|
OutlandScraperFetcher,
|
||||||
|
]
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (BookMetadata, List[BookMetadata]) where the first element is the metadata object and the second element is a list of metadata objects from all the fetchers
|
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.
|
||||||
"""
|
"""
|
||||||
#clean up isbn
|
|
||||||
isbn = isbn.replace('-', '').replace('_', '').strip().lower()
|
isbn = isbn.replace('-', '').replace('_', '').strip().lower()
|
||||||
#make sure isbn is valid
|
|
||||||
if len(isbn) != 10 and len(isbn) != 13 and not isbn.isnumeric():
|
if len(isbn) != 10 and len(isbn) != 13 and not isbn.isnumeric():
|
||||||
raise ValueError('Invalid ISBN')
|
raise ValueError('Invalid ISBN')
|
||||||
|
|
||||||
# list of fetchers to fetch book metadata from
|
results: list[BookMetadata] = []
|
||||||
open_library = OpenLibraryFetcher(isbn=isbn)
|
|
||||||
google = GoogleBooksFetcher(isbn=isbn)
|
|
||||||
storygraph = StorygraphFetcher(isbn=isbn)
|
|
||||||
outland = OutlandScraperFetcher(isbn=isbn)
|
|
||||||
|
|
||||||
#... create more fetchers objects here ...
|
with ThreadPoolExecutor() as executor:
|
||||||
|
futures = [executor.submit(fetcher.fetch_metadata, isbn) for fetcher in FETCHERS]
|
||||||
|
|
||||||
# Create a list of the objects in priority order
|
for future in futures:
|
||||||
fetchers = [
|
result = future.result()
|
||||||
open_library,
|
|
||||||
google,
|
|
||||||
storygraph,
|
|
||||||
outland
|
|
||||||
#... add more fetchers here ...
|
|
||||||
]
|
|
||||||
|
|
||||||
metadata = None
|
|
||||||
|
|
||||||
|
|
||||||
timeout = 15 # timeout in seconds for each fetcher
|
|
||||||
|
|
||||||
# make thereads for each fetcher object and run the fetch_metadata() method for the fetcher in the thread with the given isbn as parameter collect the results in a list with a reference to the fetcher object
|
|
||||||
results: List[BookMetadata] = [] # list of results in the same order as the fetchers list
|
|
||||||
threads = [] # list of threads in the same order as the fetchers list
|
|
||||||
|
|
||||||
def fetch_metadata(fetcher, isbn):
|
|
||||||
index: int = fetchers.index(fetcher)
|
|
||||||
try:
|
|
||||||
data: BookMetadata = fetcher.fetch_metadata(isbn)
|
|
||||||
results.insert(index, data)
|
|
||||||
except Exception as e:
|
|
||||||
results.insert(index, None)
|
|
||||||
|
|
||||||
for fetcher in fetchers:
|
|
||||||
thread = threading.Thread(target=fetch_metadata, args=(fetcher, isbn))
|
|
||||||
threads.append(thread)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
for thread in threads:
|
|
||||||
thread.join(timeout)
|
|
||||||
|
|
||||||
# iterate over the results list and return the first non-None result
|
|
||||||
for result in results:
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
#add non none result values to metadata where metadata has a none value
|
results.append(result)
|
||||||
if (metadata == None):
|
|
||||||
metadata = result
|
|
||||||
elif (metadata.isbn == None):
|
|
||||||
metadata.isbn = result.isbn
|
|
||||||
elif (metadata.title == None):
|
|
||||||
metadata.title = result.title
|
|
||||||
elif (metadata.authors == None or len(metadata.authors) == 0 or len(metadata.authors) < len(result.authors)):
|
|
||||||
metadata.authors = result.authors
|
|
||||||
elif (metadata.language == None):
|
|
||||||
metadata.language = result.language
|
|
||||||
elif (metadata.publish_date == None):
|
|
||||||
metadata.publish_date = result.publish_date
|
|
||||||
elif (metadata.num_pages == None):
|
|
||||||
metadata.num_pages = result.num_pages
|
|
||||||
elif (metadata.subjects == None or len(metadata.subjects) == 0 or len(metadata.subjects) < len(result.subjects)):
|
|
||||||
metadata.subjects = result.subjects
|
|
||||||
elif (metadata.isbn == None):
|
|
||||||
metadata.isbn = result.isbn
|
|
||||||
|
|
||||||
return metadata, results
|
for result in results:
|
||||||
|
try:
|
||||||
|
result.validate()
|
||||||
|
except ValueError as e:
|
||||||
|
if strict:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
print(f'Invalid metadata: {e}')
|
||||||
|
results.remove(result)
|
||||||
|
|
||||||
def fetcher_dict(isbn:str) -> dict:
|
return sort_metadata_by_priority(results)
|
||||||
"""_summary_
|
|
||||||
This function fetches book metadata from multiple sources in threads and returns the higest ranked non-None result.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
isbn (str): isbn of the book to fetch metadata for
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: a dictionary of the metadata object
|
|
||||||
"""
|
|
||||||
metadata = fetcher(isbn)[0]
|
|
||||||
if metadata is None:
|
|
||||||
return None
|
|
||||||
elif len(metadata.to_dict().keys()) == 0:
|
|
||||||
return None
|
|
||||||
metadata = metadata.to_dict() # convert to dict to make it easier to work with
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
from pprint import pprint
|
||||||
isbn = '0132624788'
|
isbn = '0132624788'
|
||||||
metadata = fetcher(isbn)[0]
|
metadata = fetch_metadata_from_multiple_sources(isbn)
|
||||||
print(metadata)
|
pprint(metadata)
|
|
@ -1,4 +1,5 @@
|
||||||
import csv
|
import csv
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
@ -6,7 +7,11 @@ from sqlalchemy.orm import Session
|
||||||
from worblehat.flaskapp.database import db
|
from worblehat.flaskapp.database import db
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
|
Author,
|
||||||
Bookcase,
|
Bookcase,
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseItemBorrowingQueue,
|
||||||
BookcaseShelf,
|
BookcaseShelf,
|
||||||
Language,
|
Language,
|
||||||
MediaType,
|
MediaType,
|
||||||
|
@ -117,6 +122,100 @@ def seed_data(sql_session: Session = db.session):
|
||||||
BookcaseShelf(row=0, column=4, bookcase=bookcases[4], description="Religion"),
|
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:
|
with open(Path(__file__).parent.parent.parent / 'data' / 'iso639_1.csv') as f:
|
||||||
reader = csv.reader(f)
|
reader = csv.reader(f)
|
||||||
languages = [Language(name, code) for (code, name) in reader]
|
languages = [Language(name, code) for (code, name) in reader]
|
||||||
|
@ -125,5 +224,9 @@ def seed_data(sql_session: Session = db.session):
|
||||||
sql_session.add_all(bookcases)
|
sql_session.add_all(bookcases)
|
||||||
sql_session.add_all(shelfs)
|
sql_session.add_all(shelfs)
|
||||||
sql_session.add_all(languages)
|
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()
|
sql_session.commit()
|
||||||
print("Added test media types, bookcases and shelfs.")
|
print("Added test media types, bookcases and shelfs.")
|
Loading…
Reference in New Issue