From ddad3f4fd06baed4937e0d9bdaf9aa155cae9a75 Mon Sep 17 00:00:00 2001 From: Adrien Pensart Date: Sat, 11 Nov 2023 09:33:20 -0500 Subject: [PATCH] use edgedb python codegen --- README.rst | 39 +- dbschema/default.esdl | 46 ++- ...ianlcvpose52w7fdmesc6vcv3l73mlb5p4a.edgeql | 4 + dbschema/migrations/00001.edgeql | 65 ++-- dbschema/migrations/00002.edgeql | 50 --- dbschema/migrations/00003.edgeql | 5 - dbschema/migrations/00004.edgeql | 9 - dbschema/migrations/00005.edgeql | 16 - doc/coverage.svg | 6 +- full-check.sh | 3 +- linting.sh | 2 + musicbot/__init__.py | 4 +- musicbot/commands/database.py | 6 +- musicbot/commands/folder.py | 7 +- musicbot/commands/local.py | 37 -- musicbot/commands/music.py | 6 +- musicbot/commands/spotify.py | 7 +- musicbot/commands/youtube.py | 5 +- musicbot/file.py | 6 +- musicbot/music.py | 6 +- musicbot/music_filter.py | 1 + musicbot/musicdb.py | 96 ++--- musicbot/queries.py | 257 -------------- musicbot/queries/gen_bests.edgeql | 147 ++++++++ musicbot/queries/gen_bests_async_edgeql.py | 334 ++++++++++++++++++ musicbot/queries/gen_playlist.edgeql | 31 ++ musicbot/queries/gen_playlist_async_edgeql.py | 144 ++++++++ musicbot/queries/remove.edgeql | 3 + musicbot/queries/remove_async_edgeql.py | 40 +++ musicbot/queries/soft_clean.edgeql | 7 + musicbot/queries/soft_clean_async_edgeql.py | 46 +++ musicbot/queries/upsert.edgeql | 89 +++++ musicbot/queries/upsert_async_edgeql.py | 202 +++++++++++ musicbot/queries_bak.py | 256 ++++++++++++++ musicbot/search_results.py | 19 - poetry.lock | 155 ++++---- pyproject.toml | 3 + recreate-prod.sh | 8 + recreate-tests.sh => recreate-test.sh | 0 tests/conftest.py | 7 +- tests/test_local.py | 19 - 41 files changed, 1536 insertions(+), 657 deletions(-) create mode 100644 dbschema/fixups/m1tklogxiitcxuoe6oapcrpljmwhpboqjgzujhj5ffrwkx5px6bxva-m1oy3zaoi4vykcutcfrianlcvpose52w7fdmesc6vcv3l73mlb5p4a.edgeql delete mode 100644 dbschema/migrations/00002.edgeql delete mode 100644 dbschema/migrations/00003.edgeql delete mode 100644 dbschema/migrations/00004.edgeql delete mode 100644 dbschema/migrations/00005.edgeql delete mode 100644 musicbot/queries.py create mode 100644 musicbot/queries/gen_bests.edgeql create mode 100644 musicbot/queries/gen_bests_async_edgeql.py create mode 100644 musicbot/queries/gen_playlist.edgeql create mode 100644 musicbot/queries/gen_playlist_async_edgeql.py create mode 100644 musicbot/queries/remove.edgeql create mode 100644 musicbot/queries/remove_async_edgeql.py create mode 100644 musicbot/queries/soft_clean.edgeql create mode 100644 musicbot/queries/soft_clean_async_edgeql.py create mode 100644 musicbot/queries/upsert.edgeql create mode 100644 musicbot/queries/upsert_async_edgeql.py create mode 100644 musicbot/queries_bak.py delete mode 100644 musicbot/search_results.py create mode 100755 recreate-prod.sh rename recreate-tests.sh => recreate-test.sh (100%) diff --git a/README.rst b/README.rst index 1028bd4..2adbfba 100644 --- a/README.rst +++ b/README.rst @@ -412,7 +412,6 @@ musicbot local playlist Generate a new playlist remove (delete) Remove one or more music scan Load musics - search (find) Search musics by full-text search sync Copy selected musics with filters to destination folder watch (watcher) Watch files changes in folders @@ -450,8 +449,8 @@ musicbot local bests Filter options: --prefilter [bests-4.0|bests-4.5|bests-5.0|no-album|no-artist|no-genre|no-keyword|no-rating|no-title|to-fix] Music pre filters (repeatable) - --filter TEXT Music filters (repeatable), fields: genre,keyword,artist,title,album,min_size,max_size - ,min_length,max_length,min_rating,max_rating,limit + --filter TEXT Music filters (repeatable), fields: genre,keyword,artist,title,album,pattern,min_size, + max_size,min_length,max_length,min_rating,max_rating,limit MusicDB options: --dsn TEXT DSN to MusicBot EdgeDB --graphql TEXT DSN to MusicBot GrapQL @@ -520,8 +519,8 @@ musicbot local player Filter options: --prefilter [bests-4.0|bests-4.5|bests-5.0|no-album|no-artist|no-genre|no-keyword|no-rating|no-title|to-fix] Music pre filters (repeatable) - --filter TEXT Music filters (repeatable), fields: genre,keyword,artist,title,album,min_size,max_size - ,min_length,max_length,min_rating,max_rating,limit + --filter TEXT Music filters (repeatable), fields: genre,keyword,artist,title,album,pattern,min_size, + max_size,min_length,max_length,min_rating,max_rating,limit Links options: --kind, --kinds [all|local|local-http|local-ssh|remote|remote-http|remote-ssh] Generate musics paths of types [default: local] @@ -557,8 +556,8 @@ musicbot local playlist Filter options: --prefilter [bests-4.0|bests-4.5|bests-5.0|no-album|no-artist|no-genre|no-keyword|no-rating|no-title|to-fix] Music pre filters (repeatable) - --filter TEXT Music filters (repeatable), fields: genre,keyword,artist,title,album,min_size,max_size - ,min_length,max_length,min_rating,max_rating,limit + --filter TEXT Music filters (repeatable), fields: genre,keyword,artist,title,album,pattern,min_size, + max_size,min_length,max_length,min_rating,max_rating,limit Links options: --kind, --kinds [all|local|local-http|local-ssh|remote|remote-http|remote-ssh] Generate musics paths of types [default: local] @@ -604,28 +603,6 @@ musicbot local scan --coroutines INTEGER Limit number of coroutines [default: 64] -h, --help Show this message and exit. -musicbot local search -********************* -.. code-block:: - - Usage: musicbot local search [OPTIONS] PATTERN - - Search musics by full-text search - - Options: - MusicDB options: - --dsn TEXT DSN to MusicBot EdgeDB - --graphql TEXT DSN to MusicBot GrapQL - --output [json|table|m3u] Output format [default: table] - Links options: - --kind, --kinds [all|local|local-http|local-ssh|remote|remote-http|remote-ssh] - Generate musics paths of types [default: local] - --relative / --no-relative Generate relative links [default: no-relative] - Ordering options: [mutually_exclusive] - --shuffle / --no-shuffle Randomize selection [default: no-shuffle] - --interleave / --no-interleave Interleave tracks by artist [default: no-interleave] - -h, --help Show this message and exit. - musicbot local sync ******************* .. code-block:: @@ -652,8 +629,8 @@ musicbot local sync Filter options: --prefilter [bests-4.0|bests-4.5|bests-5.0|no-album|no-artist|no-genre|no-keyword|no-rating|no-title|to-fix] Music pre filters (repeatable) - --filter TEXT Music filters (repeatable), fields: genre,keyword,artist,title,album,min_size,max_size - ,min_length,max_length,min_rating,max_rating,limit + --filter TEXT Music filters (repeatable), fields: genre,keyword,artist,title,album,pattern,min_size, + max_size,min_length,max_length,min_rating,max_rating,limit --flat Do not create subfolders --delete Delete files on destination if not present in library -h, --help Show this message and exit. diff --git a/dbschema/default.esdl b/dbschema/default.esdl index d3e19b1..0adf65c 100644 --- a/dbschema/default.esdl +++ b/dbschema/default.esdl @@ -11,7 +11,14 @@ module default { # required identity: ext::auth::Identity; # } - function bytes_to_human(size: int64, k: int64 = 1000, decimals: int64 = 2, units: array = [' B', ' KB', ' MB', ' GB', ' TB', ' PB', ' EB', ' ZB', ' YB']) -> str { + # alias MATCH_ALL := "(.*?)"; + + function bytes_to_human( + size: int64, + k: int64 = 1000, + decimals: int64 = 2, + units: array = [' B', ' KB', ' MB', ' GB', ' TB', ' PB', ' EB', ' ZB', ' YB'], + ) -> str { using ( select '0' ++ units[0] if size = 0 else ( @@ -307,6 +314,43 @@ module default { ) ); } + + function gen_playlist( + named only min_length: Length = 0, + named only max_length: Length = 2147483647, + named only min_size: Size = 0, + named only max_size: Size = 2147483647, + named only min_rating: Rating = 0.0, + named only max_rating: Rating = 5.0, + named only artist: str = "(.*?)", + named only album: str = "(.*?)", + named only genre: str = "(.*?)", + named only title: str = "(.*?)", + named only keyword: str = "(.*?)", + named only `limit`: `Limit` = 2147483647, + named only pattern: str = "", + ) -> set of Music { + using ( + select Music + filter + .length >= min_length and .length <= max_length + and .size >= min_size and .size <= max_size + and .rating >= min_rating and .rating <= max_rating + and re_test(artist, .artist.name) + and re_test(album, .album.name) + and re_test(genre, .genre.name) + and re_test(title, .name) + and re_test(keyword, array_join(array_agg((select .keywords.name)), " ")) + and (pattern = "" or ext::pg_trgm::word_similar(pattern, .title)) + order by + .artist.name then + .album.name then + .track then + .name + limit `limit` + ); + annotation title := "Generate a playlist from parameters"; + }; } using extension graphql; diff --git a/dbschema/fixups/m1tklogxiitcxuoe6oapcrpljmwhpboqjgzujhj5ffrwkx5px6bxva-m1oy3zaoi4vykcutcfrianlcvpose52w7fdmesc6vcv3l73mlb5p4a.edgeql b/dbschema/fixups/m1tklogxiitcxuoe6oapcrpljmwhpboqjgzujhj5ffrwkx5px6bxva-m1oy3zaoi4vykcutcfrianlcvpose52w7fdmesc6vcv3l73mlb5p4a.edgeql new file mode 100644 index 0000000..25bc2aa --- /dev/null +++ b/dbschema/fixups/m1tklogxiitcxuoe6oapcrpljmwhpboqjgzujhj5ffrwkx5px6bxva-m1oy3zaoi4vykcutcfrianlcvpose52w7fdmesc6vcv3l73mlb5p4a.edgeql @@ -0,0 +1,4 @@ +CREATE MIGRATION m1ev2cbx4eudivaafci4i6ywgmpzzxntdgyxq22dbpkffcufhmqosa + ONTO m1tklogxiitcxuoe6oapcrpljmwhpboqjgzujhj5ffrwkx5px6bxva +{ +}; diff --git a/dbschema/migrations/00001.edgeql b/dbschema/migrations/00001.edgeql index 008ce12..53b2d96 100644 --- a/dbschema/migrations/00001.edgeql +++ b/dbschema/migrations/00001.edgeql @@ -1,10 +1,9 @@ -CREATE MIGRATION m1n46zkg4ec75ff6drbbztljf3svo4hvnmby2h5u2akmqnmhtpzjiq +CREATE MIGRATION m1oy3zaoi4vykcutcfrianlcvpose52w7fdmesc6vcv3l73mlb5p4a ONTO initial { - CREATE EXTENSION pgcrypto VERSION '1.3'; - CREATE EXTENSION auth VERSION '1.0'; CREATE EXTENSION edgeql_http VERSION '1.0'; CREATE EXTENSION graphql VERSION '1.0'; + CREATE EXTENSION pg_trgm VERSION '1.6'; CREATE FUNCTION default::bytes_to_human(size: std::int64, k: std::int64 = 1000, decimals: std::int64 = 2, units: array = [' B', ' KB', ' MB', ' GB', ' TB', ' PB', ' EB', ' ZB', ' YB']) -> std::str { CREATE ANNOTATION std::title := 'Convert a byte size to human readable string'; USING (SELECT @@ -16,12 +15,7 @@ CREATE MIGRATION m1n46zkg4ec75ff6drbbztljf3svo4hvnmby2h5u2akmqnmhtpzjiq )) ) ;}; - CREATE TYPE default::User { - CREATE REQUIRED LINK identity: ext::auth::Identity; - CREATE REQUIRED PROPERTY name: std::str; - }; CREATE TYPE default::Album { - CREATE REQUIRED LINK user: default::User; CREATE REQUIRED PROPERTY name: std::str; CREATE INDEX fts::index ON (fts::with_options(.name, language := fts::Language.eng)); CREATE REQUIRED PROPERTY created_at: std::datetime { @@ -59,9 +53,9 @@ CREATE MIGRATION m1n46zkg4ec75ff6drbbztljf3svo4hvnmby2h5u2akmqnmhtpzjiq CREATE PROPERTY human_size := (SELECT default::bytes_to_human(.size) ); - CREATE REQUIRED LINK user: default::User; CREATE REQUIRED PROPERTY name: std::str; - CREATE CONSTRAINT std::exclusive ON ((.name, .user, .album)); + CREATE PROPERTY track: default::Track; + CREATE CONSTRAINT std::exclusive ON ((.name, .album)); CREATE INDEX fts::index ON (fts::with_options(.name, language := fts::Language.eng)); CREATE REQUIRED PROPERTY created_at: std::datetime { SET default := (std::datetime_current()); @@ -73,7 +67,6 @@ CREATE MIGRATION m1n46zkg4ec75ff6drbbztljf3svo4hvnmby2h5u2akmqnmhtpzjiq CREATE PROPERTY human_duration := (SELECT std::to_str(.duration, 'HH24:MI:SS') ); - CREATE PROPERTY track: default::Track; CREATE REQUIRED PROPERTY updated_at: std::datetime { CREATE REWRITE INSERT @@ -94,8 +87,7 @@ CREATE MIGRATION m1n46zkg4ec75ff6drbbztljf3svo4hvnmby2h5u2akmqnmhtpzjiq }; CREATE TYPE default::Artist { CREATE REQUIRED PROPERTY name: std::str; - CREATE REQUIRED LINK user: default::User; - CREATE CONSTRAINT std::exclusive ON ((.name, .user)); + CREATE CONSTRAINT std::exclusive ON (.name); CREATE INDEX fts::index ON (fts::with_options(.name, language := fts::Language.eng)); CREATE REQUIRED PROPERTY created_at: std::datetime { SET default := (std::datetime_current()); @@ -114,7 +106,10 @@ CREATE MIGRATION m1n46zkg4ec75ff6drbbztljf3svo4hvnmby2h5u2akmqnmhtpzjiq CREATE REQUIRED LINK artist: default::Artist { ON TARGET DELETE DELETE SOURCE; }; - CREATE CONSTRAINT std::exclusive ON ((.name, .user, .artist)); + CREATE PROPERTY title := (SELECT + ((.artist.name ++ ' - ') ++ .name) + ); + CREATE CONSTRAINT std::exclusive ON ((.name, .artist)); }; ALTER TYPE default::Artist { CREATE MULTI LINK albums := (.std::round(math::mean((.musics.rating ?? {0.0})), 2) ); }; - CREATE GLOBAL default::current_user := (std::assert_single((SELECT - default::User { - id, - name - } - FILTER - (.identity = GLOBAL ext::auth::ClientTokenIdentity) - ))); + CREATE SCALAR TYPE default::`Limit` EXTENDING std::int64 { + CREATE CONSTRAINT std::min_value(0); + }; + CREATE FUNCTION default::gen_playlist(NAMED ONLY min_length: default::Length = 0, NAMED ONLY max_length: default::Length = 2147483647, NAMED ONLY min_size: default::Size = 0, NAMED ONLY max_size: default::Size = 2147483647, NAMED ONLY min_rating: default::Rating = 0.0, NAMED ONLY max_rating: default::Rating = 5.0, NAMED ONLY artist: std::str = '(.*?)', NAMED ONLY album: std::str = '(.*?)', NAMED ONLY genre: std::str = '(.*?)', NAMED ONLY title: std::str = '(.*?)', NAMED ONLY keyword: std::str = '(.*?)', NAMED ONLY `limit`: default::`Limit` = 2147483647, NAMED ONLY pattern: std::str = '') -> SET OF default::Music { + CREATE ANNOTATION std::title := 'Generate a playlist from parameters'; + USING (SELECT + default::Music FILTER + ((((((((((((.length >= min_length) AND (.length <= max_length)) AND (.size >= min_size)) AND (.size <= max_size)) AND (.rating >= min_rating)) AND (.rating <= max_rating)) AND std::re_test(artist, .artist.name)) AND std::re_test(album, .album.name)) AND std::re_test(genre, .genre.name)) AND std::re_test(title, .name)) AND std::re_test(keyword, std::array_join(std::array_agg((SELECT + .keywords.name + )), ' '))) AND ((pattern = '') OR ext::pg_trgm::word_similar(pattern, .title))) + ORDER BY + .artist.name ASC THEN + .album.name ASC THEN + .track ASC THEN + .name ASC + LIMIT + `limit` + ) + ;}; ALTER TYPE default::Artist { CREATE LINK genres := (SELECT .musics.genre @@ -431,7 +437,4 @@ CREATE MIGRATION m1n46zkg4ec75ff6drbbztljf3svo4hvnmby2h5u2akmqnmhtpzjiq std::round(math::mean((.musics.rating ?? {0.0})), 2) ); }; - CREATE SCALAR TYPE default::`Limit` EXTENDING std::int64 { - CREATE CONSTRAINT std::min_value(0); - }; }; diff --git a/dbschema/migrations/00002.edgeql b/dbschema/migrations/00002.edgeql deleted file mode 100644 index 3c006b1..0000000 --- a/dbschema/migrations/00002.edgeql +++ /dev/null @@ -1,50 +0,0 @@ -CREATE MIGRATION m1vvmk4g5onoi2ja7ote3xorjxb7x5w7phvywwcaxx36py6zqgjssq - ONTO m1n46zkg4ec75ff6drbbztljf3svo4hvnmby2h5u2akmqnmhtpzjiq -{ - DROP GLOBAL default::current_user; - ALTER TYPE default::Album { - CREATE CONSTRAINT std::exclusive ON ((.name, .artist)); - }; - ALTER TYPE default::Album { - DROP CONSTRAINT std::exclusive ON ((.name, .user, .artist)); - DROP LINK user; - }; - ALTER TYPE default::Artist { - CREATE CONSTRAINT std::exclusive ON (.name); - }; - ALTER TYPE default::Artist { - DROP CONSTRAINT std::exclusive ON ((.name, .user)); - DROP LINK user; - }; - ALTER TYPE default::Folder { - DROP CONSTRAINT std::exclusive ON ((.name, .user, .username, .ipv4)); - }; - ALTER TYPE default::Folder { - CREATE CONSTRAINT std::exclusive ON ((.name, .username, .ipv4)); - DROP LINK user; - }; - ALTER TYPE default::Genre { - CREATE CONSTRAINT std::exclusive ON (.name); - }; - ALTER TYPE default::Genre { - DROP CONSTRAINT std::exclusive ON ((.name, .user)); - DROP LINK user; - }; - ALTER TYPE default::Keyword { - CREATE CONSTRAINT std::exclusive ON (.name); - }; - ALTER TYPE default::Keyword { - DROP CONSTRAINT std::exclusive ON ((.name, .user)); - DROP LINK user; - }; - ALTER TYPE default::Music { - DROP CONSTRAINT std::exclusive ON ((.name, .user, .album)); - }; - ALTER TYPE default::Music { - CREATE CONSTRAINT std::exclusive ON ((.name, .album)); - DROP LINK user; - }; - DROP TYPE default::User; - DROP EXTENSION auth; - DROP EXTENSION pgcrypto; -}; diff --git a/dbschema/migrations/00003.edgeql b/dbschema/migrations/00003.edgeql deleted file mode 100644 index 2eb7366..0000000 --- a/dbschema/migrations/00003.edgeql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE MIGRATION m1mawhozxatfw7ecqk4ln2jv7rakw4x4ff342vyjorz7y2u7itbs4q - ONTO m1vvmk4g5onoi2ja7ote3xorjxb7x5w7phvywwcaxx36py6zqgjssq -{ - CREATE EXTENSION pg_trgm VERSION '1.6'; -}; diff --git a/dbschema/migrations/00004.edgeql b/dbschema/migrations/00004.edgeql deleted file mode 100644 index 33809ae..0000000 --- a/dbschema/migrations/00004.edgeql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE MIGRATION m1vkbcskhpl6xb77uzkdp6axbq7uudh72kwvwldvwj7wx5aw32yzna - ONTO m1mawhozxatfw7ecqk4ln2jv7rakw4x4ff342vyjorz7y2u7itbs4q -{ - ALTER TYPE default::Music { - CREATE PROPERTY title := (SELECT - ((((.artist.name ++ ' - ') ++ .album.name) ++ ' - ') ++ .name) - ); - }; -}; diff --git a/dbschema/migrations/00005.edgeql b/dbschema/migrations/00005.edgeql deleted file mode 100644 index b3669af..0000000 --- a/dbschema/migrations/00005.edgeql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE MIGRATION m1ll44jx2l454tqbeaqe6c4kssjsd3gaiuwaom5jtki22dvbgisbfa - ONTO m1vkbcskhpl6xb77uzkdp6axbq7uudh72kwvwldvwj7wx5aw32yzna -{ - ALTER TYPE default::Album { - CREATE PROPERTY title := (SELECT - ((.artist.name ++ ' - ') ++ .name) - ); - }; - ALTER TYPE default::Music { - ALTER PROPERTY title { - USING (SELECT - ((.album.title ++ ' - ') ++ .name) - ); - }; - }; -}; diff --git a/doc/coverage.svg b/doc/coverage.svg index 1618ede..6c15cac 100644 --- a/doc/coverage.svg +++ b/doc/coverage.svg @@ -9,13 +9,13 @@ - + coverage coverage - 74% - 74% + 75% + 75% diff --git a/full-check.sh b/full-check.sh index b10b24b..e4f27e8 100755 --- a/full-check.sh +++ b/full-check.sh @@ -5,9 +5,10 @@ export SETUPTOOLS_USE_DISTUTILS=stdlib set -e trap '[ $? -eq 0 ] && exit 0 || echo "$0 FAILED"' EXIT +# poetry run edgedb-py --dsn edgedb://musicbot:musicbot@127.0.0.1:5656 --tls-security insecure --dir musicbot --file musicbot/queries.py + sh linting.sh sh gen-doc.sh -sh code-format.sh sh tests-check.sh echo "Running unit tests" diff --git a/linting.sh b/linting.sh index 0f00880..69850d7 100755 --- a/linting.sh +++ b/linting.sh @@ -4,6 +4,8 @@ export SETUPTOOLS_USE_DISTUTILS=stdlib set -e +sh code-format.sh + echo "linting : ruff..." poetry run ruff musicbot tests diff --git a/musicbot/__init__.py b/musicbot/__init__.py index 460c333..ea4d428 100644 --- a/musicbot/__init__.py +++ b/musicbot/__init__.py @@ -3,6 +3,7 @@ from musicbot.config import Config from musicbot.file import File from musicbot.folder import Folder +from musicbot.helpers import syncify from musicbot.music import Music from musicbot.music_filter import MusicFilter from musicbot.musicdb import MusicDb @@ -10,9 +11,7 @@ from musicbot.playlist import Playlist from musicbot.playlist_options import PlaylistOptions from musicbot.scan_folders import ScanFolders -from musicbot.search_results import SearchResults from musicbot.spotify import Spotify -from musicbot.helpers import syncify filterwarnings(action="ignore", module=".*vlc.*", category=DeprecationWarning) @@ -27,7 +26,6 @@ "MusicDb", "Folder", "ScanFolders", - "SearchResults", "Spotify", "syncify", ] diff --git a/musicbot/commands/database.py b/musicbot/commands/database.py index 16153ff..f1c4d2d 100644 --- a/musicbot/commands/database.py +++ b/musicbot/commands/database.py @@ -6,13 +6,9 @@ from beartype import beartype from click_skeleton import AdvancedGroup +from musicbot import MusicbotObject, MusicDb, syncify from musicbot.cli.musicdb import musicdb_options from musicbot.cli.options import yes_option -from musicbot import ( - MusicDb, - MusicbotObject, - syncify -) @click.group(help="DB management", cls=AdvancedGroup, aliases=["db"]) diff --git a/musicbot/commands/folder.py b/musicbot/commands/folder.py index 82b228d..e9fc989 100644 --- a/musicbot/commands/folder.py +++ b/musicbot/commands/folder.py @@ -7,15 +7,10 @@ from click_skeleton import AdvancedGroup from rich.table import Table +from musicbot import File, MusicbotObject, Playlist, ScanFolders from musicbot.cli.file import file_options, flat_option, keywords_option from musicbot.cli.options import output_option, threads_option from musicbot.cli.scan_folders import destination_argument, scan_folders_argument -from musicbot import ( - File, - MusicbotObject, - Playlist, - ScanFolders -) logger = logging.getLogger(__name__) diff --git a/musicbot/commands/local.py b/musicbot/commands/local.py index 7fe4ef0..0fa0835 100644 --- a/musicbot/commands/local.py +++ b/musicbot/commands/local.py @@ -20,7 +20,6 @@ MusicbotObject, MusicDb, MusicFilter, - Playlist, PlaylistOptions, ScanFolders, syncify, @@ -226,42 +225,6 @@ def __call__(self, change: Change, path: str) -> bool: pass -@cli.command(help="Search musics by full-text search", aliases=["find"]) -@musicdb_options -@output_option -@playlist_options -@click.argument("pattern") -@syncify -@beartype -async def search( - musicdb: MusicDb, - output: str, - pattern: str, - playlist_options: PlaylistOptions, -) -> None: - search_results = await musicdb.search(pattern) - p = Playlist.from_edgedb( - name=pattern, - results=search_results.musics, - ) - p.print( - output=output, - playlist_options=playlist_options, - ) - - artists = [artist.name for artist in search_results.artists] - MusicbotObject.success(f"Artists found: {artists}") - - albums = [album.name for album in search_results.albums] - MusicbotObject.success(f"Albums found: {albums}") - - genres = [genre.name for genre in search_results.genres] - MusicbotObject.success(f"Genres found: {genres}") - - keywords = [keyword.name for keyword in search_results.keywords] - MusicbotObject.success(f"Keywords found: {keywords}") - - @cli.command(short_help="Generate a new playlist", help=filters_reprs) @musicdb_options @output_option diff --git a/musicbot/commands/music.py b/musicbot/commands/music.py index fe28235..3a84548 100644 --- a/musicbot/commands/music.py +++ b/musicbot/commands/music.py @@ -8,11 +8,7 @@ from mutagen import MutagenError from rich.table import Table -from musicbot import ( - File, - MusicbotObject, - syncify, -) +from musicbot import File, MusicbotObject, syncify from musicbot.cli.file import ( acoustid_api_key_option, file_argument, diff --git a/musicbot/commands/spotify.py b/musicbot/commands/spotify.py index e948b76..2d374f5 100644 --- a/musicbot/commands/spotify.py +++ b/musicbot/commands/spotify.py @@ -8,12 +8,7 @@ from fuzzywuzzy import fuzz # type: ignore from slugify import slugify -from musicbot import ( - MusicDb, - MusicbotObject, - Spotify, - syncify, -) +from musicbot import MusicbotObject, MusicDb, Spotify, syncify from musicbot.cli.musicdb import musicdb_options from musicbot.cli.options import dry_option, output_option from musicbot.cli.spotify import ( diff --git a/musicbot/commands/youtube.py b/musicbot/commands/youtube.py index d39e980..38a983f 100644 --- a/musicbot/commands/youtube.py +++ b/musicbot/commands/youtube.py @@ -8,11 +8,8 @@ from click_skeleton import AdvancedGroup from click_skeleton.helpers import seconds_to_human +from musicbot import File, MusicbotObject from musicbot.cli.file import acoustid_api_key_option, file_argument -from musicbot import ( - File, - MusicbotObject, -) logger = logging.getLogger(__name__) diff --git a/musicbot/file.py b/musicbot/file.py index cc9fe70..c67cb15 100644 --- a/musicbot/file.py +++ b/musicbot/file.py @@ -339,7 +339,7 @@ def _description(self, description: str) -> None: self.handle.tags["comment"] = description @property - def track(self) -> int: + def track(self) -> None | int: track_tag = "tracknumber" if self.extension == ".flac" else "TRCK" track = self._get_first(track_tag) try: @@ -350,10 +350,10 @@ def track(self) -> int: return -1 if n > 2**31 - 1: logger.warning(f"{self} : invalid track number {n}") - return 0 + return None return n except ValueError: - return -1 + return None @track.setter def track(self, number: int) -> None: diff --git a/musicbot/music.py b/musicbot/music.py index 4691e57..c8ed7bb 100644 --- a/musicbot/music.py +++ b/musicbot/music.py @@ -22,14 +22,14 @@ class Music(MusicbotObject): album: str artist: str genre: str - track: int size: int rating: float length: int keywords: frozenset[str] folders: frozenset[Folder] - # youtube: str | None - # spotify: str | None + track: int | None = None + # youtube: str | None = None + # spotify: str | None = None def human_repr(self) -> str: data: dict[str, Any] = asdict(self) diff --git a/musicbot/music_filter.py b/musicbot/music_filter.py index 12516e1..6ee6f65 100644 --- a/musicbot/music_filter.py +++ b/musicbot/music_filter.py @@ -26,6 +26,7 @@ class MusicFilter(MusicbotObject): artist: str = field(default=MATCH_ALL) title: str = field(default=MATCH_ALL) album: str = field(default=MATCH_ALL) + pattern: str = field(default="") min_size = field(converter=int, default=DEFAULT_MIN_SIZE) max_size = field(converter=int, default=DEFAULT_MAX_SIZE) diff --git a/musicbot/musicdb.py b/musicbot/musicdb.py index f88911a..387584e 100644 --- a/musicbot/musicdb.py +++ b/musicbot/musicdb.py @@ -21,18 +21,12 @@ from musicbot.music_filter import MusicFilter from musicbot.object import MusicbotObject from musicbot.playlist import Playlist -from musicbot.queries import ( - ARTISTS_QUERY, - BESTS_QUERY, - FOLDERS_QUERY, - PLAYLIST_QUERY, - REMOVE_PATH_QUERY, - SEARCH_QUERY, - SOFT_CLEAN_QUERY, - UPSERT_QUERY, -) +from musicbot.queries.gen_bests_async_edgeql import GenBestsResult, gen_bests +from musicbot.queries.gen_playlist_async_edgeql import GenPlaylistResult, gen_playlist +from musicbot.queries.remove_async_edgeql import remove +from musicbot.queries.soft_clean_async_edgeql import soft_clean +from musicbot.queries.upsert_async_edgeql import upsert from musicbot.scan_folders import ScanFolders -from musicbot.search_results import SearchResults logger = logging.getLogger(__name__) @@ -91,7 +85,7 @@ async def graphql_query(self, query: str) -> httpx.Response | None: @alru_cache async def folders(self) -> list[Folder]: - results = await self.client.query(FOLDERS_QUERY) + results = await self.client.query("select Folder {*} order by .name") folders = [] for result in results: logger.error(result) @@ -115,32 +109,22 @@ async def folders(self) -> list[Folder]: return folders async def artists(self) -> list[edgedb.Object]: - return await self.client.query(ARTISTS_QUERY) + return await self.client.query("select Artist {*} order by .name") - async def execute_music_filters( + async def make_playlist( self, - query: str, music_filters: frozenset[MusicFilter] = frozenset(), - ) -> set[edgedb.Object]: - logger.debug(query) - results = set() + ) -> Playlist: if not music_filters: music_filters = frozenset([MusicFilter()]) + + results = set() for music_filter in music_filters: - intermediate_results = await self.client.query( - query, + intermediate_results: list[GenPlaylistResult] = await gen_playlist( + self.client, **attr_asdict(music_filter), ) - logger.debug(f"{len(intermediate_results)} intermediate results for {music_filter}") results.update(intermediate_results) - logger.debug(f"{len(results)} results") - return results - - async def make_playlist( - self, - music_filters: frozenset[MusicFilter] = frozenset(), - ) -> Playlist: - results = await self.execute_music_filters(PLAYLIST_QUERY, music_filters) return Playlist.from_edgedb( name=" | ".join([music_filter.help_repr() for music_filter in music_filters]), results=list(results), @@ -162,7 +146,7 @@ async def soft_clean(self) -> None | edgedb.Object: self.success("cleaning orphan musics, artists, albums, genres, keywords") if self.dry: return None - return await self.client.query_single(SOFT_CLEAN_QUERY) + return await soft_clean(self.client) async def ensure_connected(self) -> AsyncIOClient: return await self.client.ensure_connected() @@ -171,7 +155,7 @@ async def remove_music_path(self, path: str) -> None | edgedb.Object: logger.debug(f"{self} : removed {path}") if self.dry: return None - return await self.client.query(REMOVE_PATH_QUERY, path=path) + return await remove(self.client, path=path) async def upsert_path( self, @@ -194,17 +178,13 @@ async def upsert_path( self.err(f"{file} : cannot upsert music without physical folder !") return None - params = dict( - query=UPSERT_QUERY, - **input_music, - ) - if self.dry: return file - result = await self.client.query_required_single(**params) + result = await upsert(self.client, **input_music) + keywords = frozenset(keyword.name for keyword in result.keywords) - folders = [Folder(path=folder.path, name=folder.name, ipv4=folder.ipv4, username=folder.username) for folder in result.folders] + folders = [Folder(path=folder.path, name=folder.name, ipv4=folder.ipv4, username=folder.username) for folder in result.folders if folder.path is not None] output_music = Music( title=result.name, artist=result.artist.name, @@ -229,7 +209,7 @@ async def upsert_path( self.err(f"{path} : no data result for query", error=error) except OSError as error: self.warn(f"{path} : unknown error", error=error) - break + return None self.err(f"{self} : too many transaction failures", errpr=last_error) return None @@ -270,7 +250,17 @@ async def make_bests( self, music_filters: frozenset[MusicFilter] = frozenset(), ) -> list[Playlist]: - results = await self.execute_music_filters(BESTS_QUERY, music_filters) + if not music_filters: + music_filters = frozenset([MusicFilter()]) + + results: list[GenBestsResult] = [] + for music_filter in music_filters: + intermediate_result: GenBestsResult = await gen_bests( + self.client, + **attr_asdict(music_filter), + ) + results.append(intermediate_result) + playlists = [] for result in results: for genre in result.genres: @@ -290,31 +280,17 @@ async def make_bests( playlists.append(playlist) for artist in result.keywords_for_artist: artist_name = artist.artist - for keyword in artist.keywords: - keyword_name = keyword.keyword - artist_keyword = f"{artist_name}{os.sep}keyword_{keyword_name.lower()}" - self.success(f"Keyword by artist {artist_keyword} : {len(keyword.musics)}") - playlist = Playlist.from_edgedb(name=artist_keyword, results=keyword.musics) + for artist_keyword in artist.keywords: + keyword_name = artist_keyword.keyword + final_artist_keyword = f"{artist_name}{os.sep}keyword_{keyword_name.lower()}" + self.success(f"Keyword by artist {final_artist_keyword} : {len(artist_keyword.musics)}") + playlist = Playlist.from_edgedb(name=final_artist_keyword, results=artist_keyword.musics) playlists.append(playlist) for ratings_for_artist in result.ratings_for_artist: artist_name = ratings_for_artist.key.artist.name - rating_name = ratings_for_artist.key.rating + rating_name = str(ratings_for_artist.key.rating) artist_rating = f"{artist_name}{os.sep}rating_{rating_name}" self.success(f"Rating by artist {artist_rating} : {len(ratings_for_artist.elements)}") playlist = Playlist.from_edgedb(name=artist_rating, results=ratings_for_artist.elements) playlists.append(playlist) return playlists - - async def search( - self, - pattern: str, - ) -> SearchResults: - result = await self.client.query_single(query=SEARCH_QUERY, pattern=pattern) - search_results = SearchResults( - musics=result.musics, - artists=result.artists, - albums=result.albums, - genres=result.genres, - keywords=result.keywords, - ) - return search_results diff --git a/musicbot/queries.py b/musicbot/queries.py deleted file mode 100644 index ee933cf..0000000 --- a/musicbot/queries.py +++ /dev/null @@ -1,257 +0,0 @@ -import string - - -class CustomStringTemplate(string.Template): - delimiter = "#" - - -FOLDERS_QUERY: str = "select Folder {*} order by .name" -ARTISTS_QUERY: str = """select Artist {*} order by .name""" - -MUSIC_FIELDS = """ -name, -size, -genre: {name}, -album: {name}, -artist: {name}, -keywords: {name}, -length, -track, -rating, -folders: { - name, - ipv4, - username, - path := @path -} -""" - -PLAYLIST_QUERY: str = CustomStringTemplate( - """ - select Music { - #music_fields - } - filter - .length >= $min_length and .length <= $max_length - and .size >= $min_size and .size <= $max_size - and .rating >= $min_rating and .rating <= $max_rating - and re_test($artist, .artist.name) - and re_test($album, .album.name) - and re_test($genre, .genre.name) - and re_test($title, .name) - and re_test($keyword, array_join(array_agg((select .keywords.name)), " ")) - order by - .artist.name then - .album.name then - .track then - .name - limit <`Limit`>$limit -""" -).substitute(music_fields=MUSIC_FIELDS) - -SOFT_CLEAN_QUERY: str = """ -select { - musics_deleted := count((delete Music filter not exists .folders)), - albums_deleted := count((delete Album filter not exists .musics)), - artists_deleted := count((delete Artist filter not exists .musics)), - genres_deleted := count((delete Genre filter not exists .musics)), - keywords_deleted := count((delete Keyword filter not exists .musics)) -}; -""" - -# SEARCH_QUERY: str = """ -# with -# musics_res := (select fts::search(Music, $pattern, weights := [0.1, 0.1, 0.1, 0.1], language := 'eng') order by .score desc), -# artists_res := (select fts::search(Artist, $pattern) order by .score desc), -# albums_res := (select fts::search(Album, $pattern) order by .score desc), -# genres_res := (select fts::search(Genre, $pattern) order by .score desc), -# keywords_res := (select fts::search(Keyword, $pattern) order by .score desc) -# select { -# musics := (select distinct musics_res.object {**}), -# artists := (select distinct artists_res.object {name}), -# albums := (select distinct albums_res.object {name}), -# genres := (select distinct genres_res.object {name}), -# keywords := (select distinct keywords_res.object {name}) -# } -# """ - -# SEARCH_QUERY: str = CustomStringTemplate( -# """ -# select Music { -# #music_fields -# } -# filter -# .name ilike $pattern or -# .genre.name ilike $pattern or -# .album.name ilike $pattern or -# .artist.name ilike $pattern or -# .keywords.name ilike $pattern or -# .paths ilike "%" ++ $pattern ++ "%" -# """ -# ).substitute(music_fields=MUSIC_FIELDS) - -SEARCH_QUERY: str = CustomStringTemplate( - """ -select { - musics := (select Music{#music_fields} filter ext::pg_trgm::word_similar($pattern, Music.title)), - albums := (select Album{*} filter ext::pg_trgm::word_similar($pattern, Album.title)), - artists := (select Artist{*} filter ext::pg_trgm::word_similar($pattern, Artist.name)), - genres := (select Genre{*} filter ext::pg_trgm::word_similar($pattern, Genre.name)), - keywords := (select Keyword{*} filter ext::pg_trgm::word_similar($pattern, Keyword.name)), -}; -""" -).substitute(music_fields=MUSIC_FIELDS) - -REMOVE_PATH_QUERY: str = """ -update Music -filter contains(.paths, $path) -set {folders := (select .folders filter @path != $path)}; -""" - -UPSERT_QUERY: str = CustomStringTemplate( - """ -with - upsert_artist := ( - insert Artist { - name := $artist - } - unless conflict on (.name) else (select Artist) - ), - upsert_album := ( - insert Album { - name := $album, - artist := upsert_artist - } - unless conflict on (.name, .artist) else (select Album) - ), - upsert_genre := ( - insert Genre { - name := $genre - } - unless conflict on (.name) else (select Genre) - ), - upsert_keywords := ( - for keyword in { array_unpack(>$keywords) } - union ( - insert Keyword { - name := keyword - } - unless conflict on (.name) - else (select Keyword) - ) - ), - upsert_folder := ( - insert Folder { - name := $folder, - username := $username, - ipv4 := $ipv4 - } - unless conflict on (.name, .username, .ipv4) else (select Folder) - ) - select ( - insert Music { - name := $title, - size := $size, - length := $length, - genre := upsert_genre, - album := upsert_album, - keywords := upsert_keywords, - track := $track, - rating := $rating, - folders := ( - select upsert_folder { - @path := $path - } - ) - } - unless conflict on (.name, .album) - else ( - update Music - set { - size := $size, - genre := upsert_genre, - album := upsert_album, - keywords := upsert_keywords, - length := $length, - track := $track, - rating := $rating, - folders += ( - select upsert_folder { - @path := $path - } - ) - } - ) - ) { - #music_fields - } -""" -).substitute(music_fields=MUSIC_FIELDS) - -BESTS_QUERY: str = CustomStringTemplate( - """ -with - musics := (#filtered_playlist), - unique_keywords := (select distinct (for music in musics union (music.keywords))) -select { - genres := ( - group musics { - #music_fields - } - by .genre - ), - keywords := ( - for unique_keyword in unique_keywords - union ( - select Keyword { - name, - musics := ( - select musics { - #music_fields - } - filter unique_keyword.name in .keywords.name - ) - } - filter .name = unique_keyword.name - ) - ), - ratings := ( - group musics { - #music_fields - } - by .rating - ), - keywords_for_artist := ( - for artist in (select distinct musics.artist) - union ( - select { - artist := artist.name, - keywords := ( - with - artist_musics := (select musics filter .artist = artist), - artist_keywords := (select distinct (for music in artist_musics union (music.keywords))) - for artist_keyword in (select artist_keywords) - union ( - select { - keyword := artist_keyword.name, - musics := ( - select artist_musics { - #music_fields - } - filter artist_keyword in .keywords - ) - } - ) - ) - } - ) - ), - ratings_for_artist := ( - group musics { - #music_fields - } - by .artist, .rating - ) -} -""" -).substitute(music_fields=MUSIC_FIELDS, filtered_playlist=PLAYLIST_QUERY) diff --git a/musicbot/queries/gen_bests.edgeql b/musicbot/queries/gen_bests.edgeql new file mode 100644 index 0000000..2596189 --- /dev/null +++ b/musicbot/queries/gen_bests.edgeql @@ -0,0 +1,147 @@ +with + musics := (select gen_playlist( + min_length := $min_length, + max_length := $max_length, + min_size := $min_size, + max_size := $max_size, + min_rating := $min_rating, + max_rating := $max_rating, + artist := $artist, + album := $album, + genre := $genre, + title := $title, + keyword := $keyword, + pattern := $pattern, + `limit` := <`Limit`>$limit, + )), + unique_keywords := (select distinct (for music in musics union (music.keywords))) +select { + genres := ( + group musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + by .genre + ), + keywords := ( + for unique_keyword in unique_keywords + union ( + select Keyword { + name, + musics := ( + select distinct musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + filter unique_keyword.name in .keywords.name + ) + } + filter .name = unique_keyword.name + ) + ), + ratings := ( + group musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + by .rating + ), + keywords_for_artist := ( + for artist in (select distinct musics.artist) + union ( + select { + artist := artist.name, + keywords := ( + with + artist_musics := (select musics filter .artist = artist), + artist_keywords := (select distinct (for music in artist_musics union (music.keywords))) + for artist_keyword in (select artist_keywords) + union ( + select { + keyword := artist_keyword.name, + musics := ( + select distinct artist_musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + filter artist_keyword in .keywords + ) + } + ) + ) + } + ) + ), + ratings_for_artist := ( + group musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + by .artist, .rating + ) +} diff --git a/musicbot/queries/gen_bests_async_edgeql.py b/musicbot/queries/gen_bests_async_edgeql.py new file mode 100644 index 0000000..05ed60a --- /dev/null +++ b/musicbot/queries/gen_bests_async_edgeql.py @@ -0,0 +1,334 @@ +# AUTOGENERATED FROM 'musicbot/queries/gen_bests.edgeql' WITH: +# $ edgedb-py --dsn edgedb://musicbot:musicbot@127.0.0.1:5656 --tls-security insecure --dir musicbot/queries + + +from __future__ import annotations + +import dataclasses +import uuid + +import edgedb + +Length = int +Length02 = int +Limit = int +Rating = float +Rating02 = float +Size = int +Size02 = int +Track = int + + +class NoPydanticValidation: + @classmethod + def __get_validators__(cls): + from pydantic.dataclasses import dataclass as pydantic_dataclass + + pydantic_dataclass(cls) + cls.__pydantic_model__.__get_validators__ = lambda: [] + return [] + + +@dataclasses.dataclass +class GenBestsResult(NoPydanticValidation): + id: uuid.UUID + genres: list[GenBestsResultGenresItem] + keywords: list[GenBestsResultKeywordsItem] + ratings: list[GenBestsResultRatingsItem] + keywords_for_artist: list[GenBestsResultKeywordsForArtistItem] + ratings_for_artist: list[GenBestsResultRatingsForArtistItem] + + +@dataclasses.dataclass +class GenBestsResultGenresItem(NoPydanticValidation): + id: uuid.UUID + key: GenBestsResultGenresItemKey + grouping: list[str] + elements: list[GenBestsResultGenresItemElementsItem] + + +@dataclasses.dataclass +class GenBestsResultGenresItemElementsItem(NoPydanticValidation): + id: uuid.UUID + name: str + size: Size + genre: GenBestsResultGenresItemKeyGenre + album: GenBestsResultGenresItemElementsItemAlbum + artist: GenBestsResultGenresItemElementsItemArtist + keywords: list[GenBestsResultGenresItemElementsItemKeywordsItem] + length: Length + track: Track | None + rating: Rating + folders: list[GenBestsResultGenresItemElementsItemFoldersItem] + + +@dataclasses.dataclass +class GenBestsResultGenresItemElementsItemAlbum(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class GenBestsResultGenresItemElementsItemArtist(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class GenBestsResultGenresItemElementsItemFoldersItem(NoPydanticValidation): + id: uuid.UUID + name: str + ipv4: str + username: str + path: str | None + + +@dataclasses.dataclass +class GenBestsResultGenresItemElementsItemKeywordsItem(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class GenBestsResultGenresItemKey(NoPydanticValidation): + id: uuid.UUID + genre: GenBestsResultGenresItemKeyGenre + + +@dataclasses.dataclass +class GenBestsResultGenresItemKeyGenre(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class GenBestsResultKeywordsForArtistItem(NoPydanticValidation): + id: uuid.UUID + artist: str + keywords: list[GenBestsResultKeywordsForArtistItemKeywordsItem] + + +@dataclasses.dataclass +class GenBestsResultKeywordsForArtistItemKeywordsItem(NoPydanticValidation): + id: uuid.UUID + keyword: str + musics: list[GenBestsResultGenresItemElementsItem] + + +@dataclasses.dataclass +class GenBestsResultKeywordsItem(NoPydanticValidation): + id: uuid.UUID + name: str + musics: list[GenBestsResultGenresItemElementsItem] + + +@dataclasses.dataclass +class GenBestsResultRatingsForArtistItem(NoPydanticValidation): + id: uuid.UUID + key: GenBestsResultRatingsForArtistItemKey + grouping: list[str] + elements: list[GenBestsResultGenresItemElementsItem] + + +@dataclasses.dataclass +class GenBestsResultRatingsForArtistItemKey(NoPydanticValidation): + id: uuid.UUID + artist: GenBestsResultGenresItemElementsItemArtist + rating: Rating + + +@dataclasses.dataclass +class GenBestsResultRatingsItem(NoPydanticValidation): + id: uuid.UUID + key: GenBestsResultRatingsItemKey + grouping: list[str] + elements: list[GenBestsResultGenresItemElementsItem] + + +@dataclasses.dataclass +class GenBestsResultRatingsItemKey(NoPydanticValidation): + id: uuid.UUID + rating: Rating + + +async def gen_bests( + executor: edgedb.AsyncIOExecutor, + *, + min_length: Length02, + max_length: Length02, + min_size: Size02, + max_size: Size02, + min_rating: Rating02, + max_rating: Rating02, + artist: str, + album: str, + genre: str, + title: str, + keyword: str, + pattern: str, + limit: Limit, +) -> GenBestsResult: + return await executor.query_single( + """\ + with + musics := (select gen_playlist( + min_length := $min_length, + max_length := $max_length, + min_size := $min_size, + max_size := $max_size, + min_rating := $min_rating, + max_rating := $max_rating, + artist := $artist, + album := $album, + genre := $genre, + title := $title, + keyword := $keyword, + pattern := $pattern, + `limit` := <`Limit`>$limit, + )), + unique_keywords := (select distinct (for music in musics union (music.keywords))) + select { + genres := ( + group musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + by .genre + ), + keywords := ( + for unique_keyword in unique_keywords + union ( + select Keyword { + name, + musics := ( + select distinct musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + filter unique_keyword.name in .keywords.name + ) + } + filter .name = unique_keyword.name + ) + ), + ratings := ( + group musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + by .rating + ), + keywords_for_artist := ( + for artist in (select distinct musics.artist) + union ( + select { + artist := artist.name, + keywords := ( + with + artist_musics := (select musics filter .artist = artist), + artist_keywords := (select distinct (for music in artist_musics union (music.keywords))) + for artist_keyword in (select artist_keywords) + union ( + select { + keyword := artist_keyword.name, + musics := ( + select distinct artist_musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + filter artist_keyword in .keywords + ) + } + ) + ) + } + ) + ), + ratings_for_artist := ( + group musics { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + } + by .artist, .rating + ) + }\ + """, + min_length=min_length, + max_length=max_length, + min_size=min_size, + max_size=max_size, + min_rating=min_rating, + max_rating=max_rating, + artist=artist, + album=album, + genre=genre, + title=title, + keyword=keyword, + pattern=pattern, + limit=limit, + ) diff --git a/musicbot/queries/gen_playlist.edgeql b/musicbot/queries/gen_playlist.edgeql new file mode 100644 index 0000000..d5e3c24 --- /dev/null +++ b/musicbot/queries/gen_playlist.edgeql @@ -0,0 +1,31 @@ +select gen_playlist( + min_length := $min_length, + max_length := $max_length, + min_size := $min_size, + max_size := $max_size, + min_rating := $min_rating, + max_rating := $max_rating, + artist := $artist, + album := $album, + genre := $genre, + title := $title, + keyword := $keyword, + pattern := $pattern, + `limit` := <`Limit`>$limit, +) { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } +} diff --git a/musicbot/queries/gen_playlist_async_edgeql.py b/musicbot/queries/gen_playlist_async_edgeql.py new file mode 100644 index 0000000..1704882 --- /dev/null +++ b/musicbot/queries/gen_playlist_async_edgeql.py @@ -0,0 +1,144 @@ +# AUTOGENERATED FROM 'musicbot/queries/gen_playlist.edgeql' WITH: +# $ edgedb-py --dsn edgedb://musicbot:musicbot@127.0.0.1:5656 --tls-security insecure --dir musicbot/queries + + +from __future__ import annotations + +import dataclasses +import uuid + +import edgedb + +Length = int +Length02 = int +Limit = int +Rating = float +Rating02 = float +Size = int +Size02 = int +Track = int + + +class NoPydanticValidation: + @classmethod + def __get_validators__(cls): + from pydantic.dataclasses import dataclass as pydantic_dataclass + + pydantic_dataclass(cls) + cls.__pydantic_model__.__get_validators__ = lambda: [] + return [] + + +@dataclasses.dataclass +class GenPlaylistResult(NoPydanticValidation): + id: uuid.UUID + name: str + size: Size + genre: GenPlaylistResultGenre + album: GenPlaylistResultAlbum + artist: GenPlaylistResultArtist + keywords: list[GenPlaylistResultKeywordsItem] + length: Length + track: Track | None + rating: Rating + folders: list[GenPlaylistResultFoldersItem] + + +@dataclasses.dataclass +class GenPlaylistResultAlbum(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class GenPlaylistResultArtist(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class GenPlaylistResultFoldersItem(NoPydanticValidation): + id: uuid.UUID + name: str + ipv4: str + username: str + path: str | None + + +@dataclasses.dataclass +class GenPlaylistResultGenre(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class GenPlaylistResultKeywordsItem(NoPydanticValidation): + id: uuid.UUID + name: str + + +async def gen_playlist( + executor: edgedb.AsyncIOExecutor, + *, + min_length: Length02, + max_length: Length02, + min_size: Size02, + max_size: Size02, + min_rating: Rating02, + max_rating: Rating02, + artist: str, + album: str, + genre: str, + title: str, + keyword: str, + pattern: str, + limit: Limit, +) -> list[GenPlaylistResult]: + return await executor.query( + """\ + select gen_playlist( + min_length := $min_length, + max_length := $max_length, + min_size := $min_size, + max_size := $max_size, + min_rating := $min_rating, + max_rating := $max_rating, + artist := $artist, + album := $album, + genre := $genre, + title := $title, + keyword := $keyword, + pattern := $pattern, + `limit` := <`Limit`>$limit, + ) { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + }\ + """, + min_length=min_length, + max_length=max_length, + min_size=min_size, + max_size=max_size, + min_rating=min_rating, + max_rating=max_rating, + artist=artist, + album=album, + genre=genre, + title=title, + keyword=keyword, + pattern=pattern, + limit=limit, + ) diff --git a/musicbot/queries/remove.edgeql b/musicbot/queries/remove.edgeql new file mode 100644 index 0000000..64701a4 --- /dev/null +++ b/musicbot/queries/remove.edgeql @@ -0,0 +1,3 @@ +update Music +filter contains(.paths, $path) +set {folders := (select .folders filter @path != $path)}; diff --git a/musicbot/queries/remove_async_edgeql.py b/musicbot/queries/remove_async_edgeql.py new file mode 100644 index 0000000..e64df9f --- /dev/null +++ b/musicbot/queries/remove_async_edgeql.py @@ -0,0 +1,40 @@ +# AUTOGENERATED FROM 'musicbot/queries/remove.edgeql' WITH: +# $ edgedb-py --dsn edgedb://musicbot:musicbot@127.0.0.1:5656 --tls-security insecure --dir musicbot/queries + + +from __future__ import annotations + +import dataclasses +import uuid + +import edgedb + + +class NoPydanticValidation: + @classmethod + def __get_validators__(cls): + from pydantic.dataclasses import dataclass as pydantic_dataclass + + pydantic_dataclass(cls) + cls.__pydantic_model__.__get_validators__ = lambda: [] + return [] + + +@dataclasses.dataclass +class RemoveResult(NoPydanticValidation): + id: uuid.UUID + + +async def remove( + executor: edgedb.AsyncIOExecutor, + *, + path: str, +) -> list[RemoveResult]: + return await executor.query( + """\ + update Music + filter contains(.paths, $path) + set {folders := (select .folders filter @path != $path)};\ + """, + path=path, + ) diff --git a/musicbot/queries/soft_clean.edgeql b/musicbot/queries/soft_clean.edgeql new file mode 100644 index 0000000..50e9a07 --- /dev/null +++ b/musicbot/queries/soft_clean.edgeql @@ -0,0 +1,7 @@ +select { + musics_deleted := count((delete Music filter not exists .folders)), + albums_deleted := count((delete Album filter not exists .musics)), + artists_deleted := count((delete Artist filter not exists .musics)), + genres_deleted := count((delete Genre filter not exists .musics)), + keywords_deleted := count((delete Keyword filter not exists .musics)) +}; diff --git a/musicbot/queries/soft_clean_async_edgeql.py b/musicbot/queries/soft_clean_async_edgeql.py new file mode 100644 index 0000000..e6949f6 --- /dev/null +++ b/musicbot/queries/soft_clean_async_edgeql.py @@ -0,0 +1,46 @@ +# AUTOGENERATED FROM 'musicbot/queries/soft_clean.edgeql' WITH: +# $ edgedb-py --dsn edgedb://musicbot:musicbot@127.0.0.1:5656 --tls-security insecure --dir musicbot/queries + + +from __future__ import annotations + +import dataclasses +import uuid + +import edgedb + + +class NoPydanticValidation: + @classmethod + def __get_validators__(cls): + from pydantic.dataclasses import dataclass as pydantic_dataclass + + pydantic_dataclass(cls) + cls.__pydantic_model__.__get_validators__ = lambda: [] + return [] + + +@dataclasses.dataclass +class SoftCleanResult(NoPydanticValidation): + id: uuid.UUID + musics_deleted: int + albums_deleted: int + artists_deleted: int + genres_deleted: int + keywords_deleted: int + + +async def soft_clean( + executor: edgedb.AsyncIOExecutor, +) -> SoftCleanResult: + return await executor.query_single( + """\ + select { + musics_deleted := count((delete Music filter not exists .folders)), + albums_deleted := count((delete Album filter not exists .musics)), + artists_deleted := count((delete Artist filter not exists .musics)), + genres_deleted := count((delete Genre filter not exists .musics)), + keywords_deleted := count((delete Keyword filter not exists .musics)) + };\ + """, + ) diff --git a/musicbot/queries/upsert.edgeql b/musicbot/queries/upsert.edgeql new file mode 100644 index 0000000..26809d5 --- /dev/null +++ b/musicbot/queries/upsert.edgeql @@ -0,0 +1,89 @@ +with + upsert_artist := ( + insert Artist { + name := $artist + } + unless conflict on (.name) else (select Artist) + ), + upsert_album := ( + insert Album { + name := $album, + artist := upsert_artist + } + unless conflict on (.name, .artist) else (select Album) + ), + upsert_genre := ( + insert Genre { + name := $genre + } + unless conflict on (.name) else (select Genre) + ), + upsert_keywords := ( + for keyword in { array_unpack(>$keywords) } + union ( + insert Keyword { + name := keyword + } + unless conflict on (.name) + else (select Keyword) + ) + ), + upsert_folder := ( + insert Folder { + name := $folder, + username := $username, + ipv4 := $ipv4 + } + unless conflict on (.name, .username, .ipv4) else (select Folder) + ) + select ( + insert Music { + name := $title, + size := $size, + length := $length, + genre := upsert_genre, + album := upsert_album, + keywords := upsert_keywords, + track := $track, + rating := $rating, + folders := ( + select upsert_folder { + @path := $path + } + ) + } + unless conflict on (.name, .album) + else ( + update Music + set { + size := $size, + genre := upsert_genre, + album := upsert_album, + keywords := upsert_keywords, + length := $length, + track := $track, + rating := $rating, + folders += ( + select upsert_folder { + @path := $path + } + ) + } + ) + ) { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } +} diff --git a/musicbot/queries/upsert_async_edgeql.py b/musicbot/queries/upsert_async_edgeql.py new file mode 100644 index 0000000..95cf3f3 --- /dev/null +++ b/musicbot/queries/upsert_async_edgeql.py @@ -0,0 +1,202 @@ +# AUTOGENERATED FROM 'musicbot/queries/upsert.edgeql' WITH: +# $ edgedb-py --dsn edgedb://musicbot:musicbot@127.0.0.1:5656 --tls-security insecure --dir musicbot/queries + + +from __future__ import annotations + +import dataclasses +import uuid + +import edgedb + +Length = int +Length02 = int +Rating = float +Rating02 = float +Size = int +Size02 = int +Track = int +Track02 = int + + +class NoPydanticValidation: + @classmethod + def __get_validators__(cls): + from pydantic.dataclasses import dataclass as pydantic_dataclass + + pydantic_dataclass(cls) + cls.__pydantic_model__.__get_validators__ = lambda: [] + return [] + + +@dataclasses.dataclass +class UpsertResult(NoPydanticValidation): + id: uuid.UUID + name: str + size: Size + genre: UpsertResultGenre + album: UpsertResultAlbum + artist: UpsertResultArtist + keywords: list[UpsertResultKeywordsItem] + length: Length + track: Track | None + rating: Rating + folders: list[UpsertResultFoldersItem] + + +@dataclasses.dataclass +class UpsertResultAlbum(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class UpsertResultArtist(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class UpsertResultFoldersItem(NoPydanticValidation): + id: uuid.UUID + name: str + ipv4: str + username: str + path: str | None + + +@dataclasses.dataclass +class UpsertResultGenre(NoPydanticValidation): + id: uuid.UUID + name: str + + +@dataclasses.dataclass +class UpsertResultKeywordsItem(NoPydanticValidation): + id: uuid.UUID + name: str + + +async def upsert( + executor: edgedb.AsyncIOExecutor, + *, + artist: str, + album: str, + genre: str, + keywords: list[str], + folder: str, + username: str, + ipv4: str, + title: str, + size: Size02, + length: Length02, + track: Track02 | None, + rating: Rating02, + path: str, +) -> UpsertResult: + return await executor.query_single( + """\ + with + upsert_artist := ( + insert Artist { + name := $artist + } + unless conflict on (.name) else (select Artist) + ), + upsert_album := ( + insert Album { + name := $album, + artist := upsert_artist + } + unless conflict on (.name, .artist) else (select Album) + ), + upsert_genre := ( + insert Genre { + name := $genre + } + unless conflict on (.name) else (select Genre) + ), + upsert_keywords := ( + for keyword in { array_unpack(>$keywords) } + union ( + insert Keyword { + name := keyword + } + unless conflict on (.name) + else (select Keyword) + ) + ), + upsert_folder := ( + insert Folder { + name := $folder, + username := $username, + ipv4 := $ipv4 + } + unless conflict on (.name, .username, .ipv4) else (select Folder) + ) + select ( + insert Music { + name := $title, + size := $size, + length := $length, + genre := upsert_genre, + album := upsert_album, + keywords := upsert_keywords, + track := $track, + rating := $rating, + folders := ( + select upsert_folder { + @path := $path + } + ) + } + unless conflict on (.name, .album) + else ( + update Music + set { + size := $size, + genre := upsert_genre, + album := upsert_album, + keywords := upsert_keywords, + length := $length, + track := $track, + rating := $rating, + folders += ( + select upsert_folder { + @path := $path + } + ) + } + ) + ) { + name, + size, + genre: {name}, + album: {name}, + artist: {name}, + keywords: {name}, + length, + track, + rating, + folders: { + name, + ipv4, + username, + path := @path + } + }\ + """, + artist=artist, + album=album, + genre=genre, + keywords=keywords, + folder=folder, + username=username, + ipv4=ipv4, + title=title, + size=size, + length=length, + track=track, + rating=rating, + path=path, + ) diff --git a/musicbot/queries_bak.py b/musicbot/queries_bak.py new file mode 100644 index 0000000..e3bf12c --- /dev/null +++ b/musicbot/queries_bak.py @@ -0,0 +1,256 @@ +# import string + +# class CustomStringTemplate(string.Template): +# delimiter = "#" +# +# +# FOLDERS_QUERY: str = "select Folder {*} order by .name" +# ARTISTS_QUERY: str = """select Artist {*} order by .name""" +# +# MUSIC_FIELDS = """ +# name, +# size, +# genre: {name}, +# album: {name}, +# artist: {name}, +# keywords: {name}, +# length, +# track, +# rating, +# folders: { +# name, +# ipv4, +# username, +# path := @path +# } +# """ +# +# PLAYLIST_QUERY: str = CustomStringTemplate( +# """ +# select Music { +# #music_fields +# } +# filter +# .length >= $min_length and .length <= $max_length +# and .size >= $min_size and .size <= $max_size +# and .rating >= $min_rating and .rating <= $max_rating +# and re_test($artist, .artist.name) +# and re_test($album, .album.name) +# and re_test($genre, .genre.name) +# and re_test($title, .name) +# and re_test($keyword, array_join(array_agg((select .keywords.name)), " ")) +# order by +# .artist.name then +# .album.name then +# .track then +# .name +# limit <`Limit`>$limit +# """ +# ).substitute(music_fields=MUSIC_FIELDS) + +# SOFT_CLEAN_QUERY: str = """ +# select { +# musics_deleted := count((delete Music filter not exists .folders)), +# albums_deleted := count((delete Album filter not exists .musics)), +# artists_deleted := count((delete Artist filter not exists .musics)), +# genres_deleted := count((delete Genre filter not exists .musics)), +# keywords_deleted := count((delete Keyword filter not exists .musics)) +# }; +# """ + +# SEARCH_QUERY: str = """ +# with +# musics_res := (select fts::search(Music, $pattern, weights := [0.1, 0.1, 0.1, 0.1], language := 'eng') order by .score desc), +# artists_res := (select fts::search(Artist, $pattern) order by .score desc), +# albums_res := (select fts::search(Album, $pattern) order by .score desc), +# genres_res := (select fts::search(Genre, $pattern) order by .score desc), +# keywords_res := (select fts::search(Keyword, $pattern) order by .score desc) +# select { +# musics := (select distinct musics_res.object {**}), +# artists := (select distinct artists_res.object {name}), +# albums := (select distinct albums_res.object {name}), +# genres := (select distinct genres_res.object {name}), +# keywords := (select distinct keywords_res.object {name}) +# } +# """ + +# SEARCH_QUERY: str = CustomStringTemplate( +# """ +# select Music { +# #music_fields +# } +# filter +# .name ilike $pattern or +# .genre.name ilike $pattern or +# .album.name ilike $pattern or +# .artist.name ilike $pattern or +# .keywords.name ilike $pattern or +# .paths ilike "%" ++ $pattern ++ "%" +# """ +# ).substitute(music_fields=MUSIC_FIELDS) + +# SEARCH_QUERY: str = CustomStringTemplate( +# """ +# select { +# musics := (select Music{#music_fields} filter ext::pg_trgm::word_similar($pattern, Music.title)), +# albums := (select Album{*} filter ext::pg_trgm::word_similar($pattern, Album.title)), +# artists := (select Artist{*} filter ext::pg_trgm::word_similar($pattern, Artist.name)), +# genres := (select Genre{*} filter ext::pg_trgm::word_similar($pattern, Genre.name)), +# keywords := (select Keyword{*} filter ext::pg_trgm::word_similar($pattern, Keyword.name)), +# }; +# """ +# ).substitute(music_fields=MUSIC_FIELDS) + +# REMOVE_PATH_QUERY: str = """ +# update Music +# filter contains(.paths, $path) +# set {folders := (select .folders filter @path != $path)}; +# """ + +# UPSERT_QUERY: str = CustomStringTemplate( +# """ +# with +# upsert_artist := ( +# insert Artist { +# name := $artist +# } +# unless conflict on (.name) else (select Artist) +# ), +# upsert_album := ( +# insert Album { +# name := $album, +# artist := upsert_artist +# } +# unless conflict on (.name, .artist) else (select Album) +# ), +# upsert_genre := ( +# insert Genre { +# name := $genre +# } +# unless conflict on (.name) else (select Genre) +# ), +# upsert_keywords := ( +# for keyword in { array_unpack(>$keywords) } +# union ( +# insert Keyword { +# name := keyword +# } +# unless conflict on (.name) +# else (select Keyword) +# ) +# ), +# upsert_folder := ( +# insert Folder { +# name := $folder, +# username := $username, +# ipv4 := $ipv4 +# } +# unless conflict on (.name, .username, .ipv4) else (select Folder) +# ) +# select ( +# insert Music { +# name := $title, +# size := $size, +# length := $length, +# genre := upsert_genre, +# album := upsert_album, +# keywords := upsert_keywords, +# track := $track, +# rating := $rating, +# folders := ( +# select upsert_folder { +# @path := $path +# } +# ) +# } +# unless conflict on (.name, .album) +# else ( +# update Music +# set { +# size := $size, +# genre := upsert_genre, +# album := upsert_album, +# keywords := upsert_keywords, +# length := $length, +# track := $track, +# rating := $rating, +# folders += ( +# select upsert_folder { +# @path := $path +# } +# ) +# } +# ) +# ) { +# #music_fields +# } +# """ +# ).substitute(music_fields=MUSIC_FIELDS) + +# BESTS_QUERY: str = CustomStringTemplate( +# """ +# with +# musics := (#filtered_playlist), +# unique_keywords := (select distinct (for music in musics union (music.keywords))) +# select { +# genres := ( +# group musics { +# #music_fields +# } +# by .genre +# ), +# keywords := ( +# for unique_keyword in unique_keywords +# union ( +# select Keyword { +# name, +# musics := ( +# select musics { +# #music_fields +# } +# filter unique_keyword.name in .keywords.name +# ) +# } +# filter .name = unique_keyword.name +# ) +# ), +# ratings := ( +# group musics { +# #music_fields +# } +# by .rating +# ), +# keywords_for_artist := ( +# for artist in (select distinct musics.artist) +# union ( +# select { +# artist := artist.name, +# keywords := ( +# with +# artist_musics := (select musics filter .artist = artist), +# artist_keywords := (select distinct (for music in artist_musics union (music.keywords))) +# for artist_keyword in (select artist_keywords) +# union ( +# select { +# keyword := artist_keyword.name, +# musics := ( +# select artist_musics { +# #music_fields +# } +# filter artist_keyword in .keywords +# ) +# } +# ) +# ) +# } +# ) +# ), +# ratings_for_artist := ( +# group musics { +# #music_fields +# } +# by .artist, .rating +# ) +# } +# """ +# ).substitute(music_fields=MUSIC_FIELDS, filtered_playlist=PLAYLIST_QUERY) diff --git a/musicbot/search_results.py b/musicbot/search_results.py deleted file mode 100644 index 2f23c2f..0000000 --- a/musicbot/search_results.py +++ /dev/null @@ -1,19 +0,0 @@ -import logging -from dataclasses import dataclass - -import edgedb -from beartype import beartype - -from musicbot.object import MusicbotObject - -logger = logging.getLogger(__name__) - - -@beartype -@dataclass(frozen=True) -class SearchResults(MusicbotObject): - musics: list[edgedb.Object] - artists: list[edgedb.Object] - albums: list[edgedb.Object] - genres: list[edgedb.Object] - keywords: list[edgedb.Object] diff --git a/poetry.lock b/poetry.lock index 46d6573..8b5befa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -267,29 +267,29 @@ test-tox-coverage = ["coverage (>=5.5)"] [[package]] name = "black" -version = "23.10.1" +version = "23.11.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, - {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, - {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, - {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, - {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, - {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, - {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, - {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, - {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, - {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, - {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, - {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, + {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, + {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, + {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, + {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, + {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, + {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, + {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, + {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, + {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, + {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, + {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, + {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, + {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, + {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, + {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, + {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, + {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, + {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, ] [package.dependencies] @@ -1099,13 +1099,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.1" +version = "1.0.2" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.1-py3-none-any.whl", hash = "sha256:c5e97ef177dca2023d0b9aad98e49507ef5423e9f1d94ffe2cfe250aa28e63b0"}, - {file = "httpcore-1.0.1.tar.gz", hash = "sha256:fce1ddf9b606cfb98132ab58865c3728c52c8e4c3c46e2aabb3674464a186e92"}, + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, ] [package.dependencies] @@ -1755,38 +1755,38 @@ files = [ [[package]] name = "mypy" -version = "1.6.1" +version = "1.7.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"}, - {file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"}, - {file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"}, - {file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"}, - {file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"}, - {file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"}, - {file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"}, - {file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"}, - {file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"}, - {file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"}, - {file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"}, - {file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"}, - {file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"}, - {file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"}, - {file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"}, - {file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"}, - {file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"}, - {file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"}, - {file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"}, - {file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"}, - {file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"}, - {file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"}, - {file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"}, - {file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"}, - {file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"}, - {file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"}, - {file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"}, + {file = "mypy-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357"}, + {file = "mypy-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f"}, + {file = "mypy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac"}, + {file = "mypy-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1"}, + {file = "mypy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9"}, + {file = "mypy-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401"}, + {file = "mypy-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d"}, + {file = "mypy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1"}, + {file = "mypy-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46"}, + {file = "mypy-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee"}, + {file = "mypy-1.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe"}, + {file = "mypy-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d"}, + {file = "mypy-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc"}, + {file = "mypy-1.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cddee95dea7990e2215576fae95f6b78a8c12f4c089d7e4367564704e99118d3"}, + {file = "mypy-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:d01921dbd691c4061a3e2ecdbfbfad029410c5c2b1ee88946bf45c62c6c91210"}, + {file = "mypy-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41"}, + {file = "mypy-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7b1e399c47b18feb6f8ad4a3eef3813e28c1e871ea7d4ea5d444b2ac03c418"}, + {file = "mypy-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391"}, + {file = "mypy-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d0fa29919d2e720c8dbaf07d5578f93d7b313c3e9954c8ec05b6d83da592e5d9"}, + {file = "mypy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5"}, + {file = "mypy-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1b06b4b109e342f7dccc9efda965fc3970a604db70f8560ddfdee7ef19afb05"}, + {file = "mypy-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8"}, + {file = "mypy-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551d4a0cdcbd1d2cccdcc7cb516bb4ae888794929f5b040bb51aae1846062901"}, + {file = "mypy-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55d28d7963bef00c330cb6461db80b0b72afe2f3c4e2963c99517cf06454e665"}, + {file = "mypy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010"}, + {file = "mypy-1.7.0-py3-none-any.whl", hash = "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96"}, + {file = "mypy-1.7.0.tar.gz", hash = "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc"}, ] [package.dependencies] @@ -1796,6 +1796,7 @@ typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -2061,13 +2062,13 @@ sqlparse = ">=0.1.19" [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.0.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, ] [package.extras] @@ -2109,13 +2110,13 @@ tests = ["flake8 (>=3.7.7)", "freezegun (>=0.3.11)", "pytest (>=4.6.9)", "pytest [[package]] name = "prompt-toolkit" -version = "3.0.39" +version = "3.0.40" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, - {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, + {file = "prompt_toolkit-3.0.40-py3-none-any.whl", hash = "sha256:99ba3dfb23d5b5af89712f89e60a5f3d9b8b67a9482ca377c5771d0e9047a34b"}, + {file = "prompt_toolkit-3.0.40.tar.gz", hash = "sha256:a371c06bb1d66cd499fecd708e50c0b6ae00acba9822ba33c586e2f16d1b739e"}, ] [package.dependencies] @@ -2453,13 +2454,13 @@ testutils = ["gitpython (>3)"] [[package]] name = "pyright" -version = "1.1.334" +version = "1.1.335" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.334-py3-none-any.whl", hash = "sha256:dcb13e8358e021189672c4d6ebcad192ab061e4c7225036973ec493183c6da68"}, - {file = "pyright-1.1.334.tar.gz", hash = "sha256:3adaf10f1f4209575dc022f9c897f7ef024639b7ea5b3cbe49302147e6949cd4"}, + {file = "pyright-1.1.335-py3-none-any.whl", hash = "sha256:1149d99d5cea3997010a5ac39611534e0426125d5090913ae5cb1e0e2c9fbca3"}, + {file = "pyright-1.1.335.tar.gz", hash = "sha256:12c09c1644b223515cc342f7d383e55eefeedd730d7875e39a2cf338c2d99be4"}, ] [package.dependencies] @@ -2876,28 +2877,28 @@ files = [ [[package]] name = "ruff" -version = "0.1.4" +version = "0.1.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.4-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:864958706b669cce31d629902175138ad8a069d99ca53514611521f532d91495"}, - {file = "ruff-0.1.4-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:9fdd61883bb34317c788af87f4cd75dfee3a73f5ded714b77ba928e418d6e39e"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4eaca8c9cc39aa7f0f0d7b8fe24ecb51232d1bb620fc4441a61161be4a17539"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a9a1301dc43cbf633fb603242bccd0aaa34834750a14a4c1817e2e5c8d60de17"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e8db8ab6f100f02e28b3d713270c857d370b8d61871d5c7d1702ae411df683"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:80fea754eaae06335784b8ea053d6eb8e9aac75359ebddd6fee0858e87c8d510"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bc02a480d4bfffd163a723698da15d1a9aec2fced4c06f2a753f87f4ce6969c"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862811b403063765b03e716dac0fda8fdbe78b675cd947ed5873506448acea4"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58826efb8b3efbb59bb306f4b19640b7e366967a31c049d49311d9eb3a4c60cb"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fdfd453fc91d9d86d6aaa33b1bafa69d114cf7421057868f0b79104079d3e66e"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e8791482d508bd0b36c76481ad3117987301b86072158bdb69d796503e1c84a8"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01206e361021426e3c1b7fba06ddcb20dbc5037d64f6841e5f2b21084dc51800"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:645591a613a42cb7e5c2b667cbefd3877b21e0252b59272ba7212c3d35a5819f"}, - {file = "ruff-0.1.4-py3-none-win32.whl", hash = "sha256:99908ca2b3b85bffe7e1414275d004917d1e0dfc99d497ccd2ecd19ad115fd0d"}, - {file = "ruff-0.1.4-py3-none-win_amd64.whl", hash = "sha256:1dfd6bf8f6ad0a4ac99333f437e0ec168989adc5d837ecd38ddb2cc4a2e3db8a"}, - {file = "ruff-0.1.4-py3-none-win_arm64.whl", hash = "sha256:d98ae9ebf56444e18a3e3652b3383204748f73e247dea6caaf8b52d37e6b32da"}, - {file = "ruff-0.1.4.tar.gz", hash = "sha256:21520ecca4cc555162068d87c747b8f95e1e95f8ecfcbbe59e8dd00710586315"}, + {file = "ruff-0.1.5-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:32d47fc69261c21a4c48916f16ca272bf2f273eb635d91c65d5cd548bf1f3d96"}, + {file = "ruff-0.1.5-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:171276c1df6c07fa0597fb946139ced1c2978f4f0b8254f201281729981f3c17"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ef33cd0bb7316ca65649fc748acc1406dfa4da96a3d0cde6d52f2e866c7b39"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2c205827b3f8c13b4a432e9585750b93fd907986fe1aec62b2a02cf4401eee6"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb408e3a2ad8f6881d0f2e7ad70cddb3ed9f200eb3517a91a245bbe27101d379"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f20dc5e5905ddb407060ca27267c7174f532375c08076d1a953cf7bb016f5a24"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aafb9d2b671ed934998e881e2c0f5845a4295e84e719359c71c39a5363cccc91"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4894dddb476597a0ba4473d72a23151b8b3b0b5f958f2cf4d3f1c572cdb7af7"}, + {file = "ruff-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00a7ec893f665ed60008c70fe9eeb58d210e6b4d83ec6654a9904871f982a2a"}, + {file = "ruff-0.1.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8c11206b47f283cbda399a654fd0178d7a389e631f19f51da15cbe631480c5b"}, + {file = "ruff-0.1.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fa29e67b3284b9a79b1a85ee66e293a94ac6b7bb068b307a8a373c3d343aa8ec"}, + {file = "ruff-0.1.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9b97fd6da44d6cceb188147b68db69a5741fbc736465b5cea3928fdac0bc1aeb"}, + {file = "ruff-0.1.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:721f4b9d3b4161df8dc9f09aa8562e39d14e55a4dbaa451a8e55bdc9590e20f4"}, + {file = "ruff-0.1.5-py3-none-win32.whl", hash = "sha256:f80c73bba6bc69e4fdc73b3991db0b546ce641bdcd5b07210b8ad6f64c79f1ab"}, + {file = "ruff-0.1.5-py3-none-win_amd64.whl", hash = "sha256:c21fe20ee7d76206d290a76271c1af7a5096bc4c73ab9383ed2ad35f852a0087"}, + {file = "ruff-0.1.5-py3-none-win_arm64.whl", hash = "sha256:82bfcb9927e88c1ed50f49ac6c9728dab3ea451212693fe40d08d314663e412f"}, + {file = "ruff-0.1.5.tar.gz", hash = "sha256:5cbec0ef2ae1748fb194f420fb03fb2c25c3258c86129af7172ff8f198f125ab"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index cac05bc..48e9812 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ include = [ "musicbot", "tests" ] +exclude = ["musicbot/queries"] reportGeneralTypeIssues = false reportPrivateImportUsage = false reportUnusedCallResult = true @@ -95,12 +96,14 @@ log_cli = true [tool.pylint.master] jobs = 4 +ignore-paths = "musicbot/queries" [tool.pylint.messages_control] disable = """ method-cache-max-size-none,too-few-public-methods,logging-format-interpolation,line-too-long,too-many-arguments,protected-access,missing-docstring,invalid-name,too-many-public-methods,too-many-instance-attributes,duplicate-code,too-many-nested-blocks,too-many-branches,too-many-return-statements,too-many-statements,too-many-locals,too-many-ancestors,abstract-method,anomalous-backslash-in-string,import-outside-toplevel,redefined-outer-name,unnecessary-lambda,c-extension-no-member,logging-fstring-interpolation,too-many-boolean-expressions,use-dict-literal,broad-except """ [tool.mypy] +exclude = "musicbot/queries" allow_redefinition = false disallow_untyped_defs = true ignore_errors = false diff --git a/recreate-prod.sh b/recreate-prod.sh new file mode 100755 index 0000000..1e4e8f6 --- /dev/null +++ b/recreate-prod.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e +trap '[ $? -eq 0 ] && exit 0 || echo "$0 FAILED"' EXIT + +docker compose rm musicbot_db_prod -s -f -v +docker compose create musicbot_db_prod +docker compose start musicbot_db_prod diff --git a/recreate-tests.sh b/recreate-test.sh similarity index 100% rename from recreate-tests.sh rename to recreate-test.sh diff --git a/tests/conftest.py b/tests/conftest.py index 2ce0f81..2b5bb01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,12 +7,7 @@ from click.testing import CliRunner from pytest import fixture, skip -from musicbot import ( - File, - MusicDb, - ScanFolders, - syncify, -) +from musicbot import File, MusicDb, ScanFolders, syncify from . import fixtures diff --git a/tests/test_local.py b/tests/test_local.py index f2c2283..ec8206b 100644 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -26,25 +26,6 @@ def test_local_folders(cli_runner: CliRunner, edgedb: str) -> None: assert MusicbotObject.loads_json(output) is not None -@beartype -def test_local_search(cli_runner: CliRunner, edgedb: str) -> None: - output = run_cli( - cli_runner, - cli, - [ - "--quiet", - "local", - "search", - "1995", - "--dsn", - edgedb, - "--output", - "json", - ], - ) - assert MusicbotObject.loads_json(output) is not None - - @beartype def test_local_remove(cli_runner: CliRunner, edgedb: str) -> None: _ = run_cli(