From b5216a06f489b5da8c1e66b86603eedfc9d15856 Mon Sep 17 00:00:00 2001 From: Vrihub Date: Sat, 15 Jun 2024 20:52:55 +0200 Subject: [PATCH 001/728] Proposed fix for issue #5218 Check for existence of "title" matching group before using it --- beetsplug/fromfilename.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 103e82901..70b9d41fb 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -98,6 +98,7 @@ def apply_matches(d, log): # Given both an "artist" and "title" field, assume that one is # *actually* the artist, which must be uniform, and use the other # for the title. This, of course, won't work for VA albums. + # Only check for "artist": patterns containing it, also contain "title" if "artist" in keys: if equal_fields(d, "artist"): artist = some_map["artist"] @@ -113,14 +114,15 @@ def apply_matches(d, log): if not item.artist: item.artist = artist log.info("Artist replaced with: {}".format(item.artist)) - - # No artist field: remaining field is the title. - else: + # otherwise, if the pattern contains "title", use that for title_field + elif "title" in keys: title_field = "title" + else: + title_field = None - # Apply the title and track. + # Apply the title and track, if any. for item in d: - if bad_title(item.title): + if title_field and bad_title(item.title): item.title = str(d[item][title_field]) log.info("Title replaced with: {}".format(item.title)) From 09660426a887c69fc6269c20a4548cd65a308df3 Mon Sep 17 00:00:00 2001 From: Vrihub Date: Sat, 15 Jun 2024 20:56:40 +0200 Subject: [PATCH 002/728] Logging: add message about the pattern being tried --- beetsplug/fromfilename.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 70b9d41fb..598cbeda9 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -162,6 +162,7 @@ class FromFilenamePlugin(plugins.BeetsPlugin): # Look for useful information in the filenames. for pattern in PATTERNS: + self._log.debug("Trying pattern: {}".format(pattern)) d = all_matches(names, pattern) if d: apply_matches(d, self._log) From e6b773561b1cb588e6e3204030092af62859bd1a Mon Sep 17 00:00:00 2001 From: Vrihub Date: Sat, 15 Jun 2024 20:58:41 +0200 Subject: [PATCH 003/728] Refactor regexps in PATTERNS --- beetsplug/fromfilename.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py index 598cbeda9..0f34fd09c 100644 --- a/beetsplug/fromfilename.py +++ b/beetsplug/fromfilename.py @@ -25,12 +25,10 @@ from beets.util import displayable_path # Filename field extraction patterns. PATTERNS = [ # Useful patterns. - r"^(?P.+)[\-_](?P.+)[\-_](?P<tag>.*)$", - r"^(?P<track>\d+)[\s.\-_]+(?P<artist>.+)[\-_](?P<title>.+)[\-_](?P<tag>.*)$", - r"^(?P<artist>.+)[\-_](?P<title>.+)$", - r"^(?P<track>\d+)[\s.\-_]+(?P<artist>.+)[\-_](?P<title>.+)$", - r"^(?P<track>\d+)[\s.\-_]+(?P<title>.+)$", - r"^(?P<track>\d+)\s+(?P<title>.+)$", + (r"^(?P<track>\d+)\.?\s*-\s*(?P<artist>.+?)\s*-\s*(?P<title>.+?)" + r"(\s*-\s*(?P<tag>.*))?$"), + r"^(?P<artist>.+?)\s*-\s*(?P<title>.+?)(\s*-\s*(?P<tag>.*))?$", + r"^(?P<track>\d+)\.?[\s\-_]+(?P<title>.+)$", r"^(?P<title>.+) by (?P<artist>.+)$", r"^(?P<track>\d+).*$", r"^(?P<title>.+)$", From 2f2680fa8d9431e3f2bebef82c2feb7aad9b1248 Mon Sep 17 00:00:00 2001 From: Vrihub <Vrihub@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:02:33 +0200 Subject: [PATCH 004/728] Added tests for the fromfilename plugin --- test/plugins/test_fromfilename.py | 143 ++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 test/plugins/test_fromfilename.py diff --git a/test/plugins/test_fromfilename.py b/test/plugins/test_fromfilename.py new file mode 100644 index 000000000..3dc600ced --- /dev/null +++ b/test/plugins/test_fromfilename.py @@ -0,0 +1,143 @@ +# This file is part of beets. +# Copyright 2016, Jan-Erik Dahlin. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Tests for the fromfilename plugin. +""" + +import unittest +from unittest.mock import Mock +from beetsplug import fromfilename + + +class FromfilenamePluginTest(unittest.TestCase): + + def setUp(self): + """Create mock objects for import session and task.""" + self.session = Mock() + + item1config = {'path': '', 'track': 0, 'artist': '', 'title': ''} + self.item1 = Mock(**item1config) + + item2config = {'path': '', 'track': 0, 'artist': '', 'title': ''} + self.item2 = Mock(**item2config) + + taskconfig = {'is_album': True, 'items': [self.item1, self.item2]} + self.task = Mock(**taskconfig) + + def tearDown(self): + del self.session, self.task, self.item1, self.item2 + + def test_sep_sds(self): + """Test filenames that use " - " as separator.""" + + self.item1.path = "/music/files/01 - Artist Name - Song One.m4a" + self.item2.path = "/music/files/02. - Artist Name - Song Two.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 1) + self.assertEqual(self.task.items[1].track, 2) + self.assertEqual(self.task.items[0].artist, "Artist Name") + self.assertEqual(self.task.items[1].artist, "Artist Name") + self.assertEqual(self.task.items[0].title, "Song One") + self.assertEqual(self.task.items[1].title, "Song Two") + + def test_sep_dash(self): + """Test filenames that use "-" as separator.""" + + self.item1.path = "/music/files/01-Artist_Name-Song_One.m4a" + self.item2.path = "/music/files/02.-Artist_Name-Song_Two.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 1) + self.assertEqual(self.task.items[1].track, 2) + self.assertEqual(self.task.items[0].artist, "Artist_Name") + self.assertEqual(self.task.items[1].artist, "Artist_Name") + self.assertEqual(self.task.items[0].title, "Song_One") + self.assertEqual(self.task.items[1].title, "Song_Two") + + def test_track_title(self): + """Test filenames including track and title.""" + + self.item1.path = "/music/files/01 - Song_One.m4a" + self.item2.path = "/music/files/02. Song_Two.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 1) + self.assertEqual(self.task.items[1].track, 2) + self.assertEqual(self.task.items[0].artist, "") + self.assertEqual(self.task.items[1].artist, "") + self.assertEqual(self.task.items[0].title, "Song_One") + self.assertEqual(self.task.items[1].title, "Song_Two") + + def test_title_by_artist(self): + """Test filenames including title by artist.""" + + self.item1.path = "/music/files/Song One by The Artist.m4a" + self.item2.path = "/music/files/Song Two by The Artist.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 0) + self.assertEqual(self.task.items[1].track, 0) + self.assertEqual(self.task.items[0].artist, "The Artist") + self.assertEqual(self.task.items[1].artist, "The Artist") + self.assertEqual(self.task.items[0].title, "Song One") + self.assertEqual(self.task.items[1].title, "Song Two") + + def test_track_only(self): + """Test filenames including only track.""" + + self.item1.path = "/music/files/01.m4a" + self.item2.path = "/music/files/02.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 1) + self.assertEqual(self.task.items[1].track, 2) + self.assertEqual(self.task.items[0].artist, "") + self.assertEqual(self.task.items[1].artist, "") + self.assertEqual(self.task.items[0].title, "01") + self.assertEqual(self.task.items[1].title, "02") + + def test_title_only(self): + """Test filenames including only title.""" + + self.item1.path = "/music/files/Song One.m4a" + self.item2.path = "/music/files/Song Two.m4a" + + f = fromfilename.FromFilenamePlugin() + f.filename_task(self.task, self.session) + + self.assertEqual(self.task.items[0].track, 0) + self.assertEqual(self.task.items[1].track, 0) + self.assertEqual(self.task.items[0].artist, "") + self.assertEqual(self.task.items[1].artist, "") + self.assertEqual(self.task.items[0].title, "Song One") + self.assertEqual(self.task.items[1].title, "Song Two") + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + + +if __name__ == "__main__": + unittest.main(defaultTest="suite") From 7acf2b3acfbd831dd7b5ab35a5985ff8854eb346 Mon Sep 17 00:00:00 2001 From: Emi Katagiri-Simpson <emi@alchemi.dev> Date: Sat, 22 Mar 2025 23:15:45 -0400 Subject: [PATCH 005/728] Dereference symlinks before hardlinking (see #5676) --- beets/util/__init__.py | 4 +++- docs/changelog.rst | 4 ++++ test/test_files.py | 18 +++++++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index b882ed626..6f51bbd52 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -605,7 +605,9 @@ def hardlink(path: bytes, dest: bytes, replace: bool = False): if os.path.exists(syspath(dest)) and not replace: raise FilesystemError("file exists", "rename", (path, dest)) try: - os.link(syspath(path), syspath(dest)) + # This step dereferences any symlinks and converts to an absolute path + resolved_origin = Path(syspath(path)).resolve() + os.link(resolved_origin, syspath(dest)) except NotImplementedError: raise FilesystemError( "OS does not support hard links." "link", diff --git a/docs/changelog.rst b/docs/changelog.rst index 88d87e32f..27fcbc24f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,10 @@ Bug fixes: * :doc:`plugins/fetchart`: Fix fetchart bug where a tempfile could not be deleted due to never being properly closed. :bug:`5521` +* When hardlinking from a symlink (e.g. importing a symlink with hardlinking + enabled), dereference the symlink then hardlink, rather than creating a new + (potentially broken) symlink + :bug:`5676` * :doc:`plugins/lyrics`: LRCLib will fallback to plain lyrics if synced lyrics are not found and `synced` flag is set to `yes`. * Synchronise files included in the source distribution with what we used to diff --git a/test/test_files.py b/test/test_files.py index 72b1610c0..c99f8f02b 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -35,7 +35,8 @@ class MoveTest(BeetsTestCase): super().setUp() # make a temporary file - self.path = join(self.temp_dir, b"temp.mp3") + self.temp_music_file_name = b"temp.mp3" + self.path = join(self.temp_dir, self.temp_music_file_name) shutil.copy( syspath(join(_common.RSRC, b"full.mp3")), syspath(self.path), @@ -199,6 +200,21 @@ class MoveTest(BeetsTestCase): self.i.move(operation=MoveOperation.HARDLINK) assert self.i.path == util.normpath(self.dest) + @unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks") + def test_hardlink_from_symlink(self): + link_path = join(self.temp_dir, b"temp_link.mp3") + link_source = join(b"./", self.temp_music_file_name) + os.symlink(syspath(link_source), syspath(link_path)) + self.i.path = link_path + self.i.move(operation=MoveOperation.HARDLINK) + + s1 = os.stat(syspath(self.path)) + s2 = os.stat(syspath(self.dest)) + assert (s1[stat.ST_INO], s1[stat.ST_DEV]) == ( + s2[stat.ST_INO], + s2[stat.ST_DEV], + ) + class HelperTest(BeetsTestCase): def test_ancestry_works_on_file(self): From 4a43191c31d0bb0eaa4454dd335634ae245aa0f5 Mon Sep 17 00:00:00 2001 From: Emi Katagiri-Simpson <emi@alchemi.dev> Date: Sun, 23 Mar 2025 15:29:41 -0400 Subject: [PATCH 006/728] BUG: Wrong path edited when running config -e Previously: ALWAYS edited the default config path Corrected: When the --config <path> option is used, that path is edited --- beets/ui/__init__.py | 2 +- beets/ui/commands.py | 9 ++++++--- docs/changelog.rst | 3 +++ test/test_config_command.py | 12 ++++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 386410a09..b478b2443 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -1839,7 +1839,7 @@ def _raw_main(args, lib=None): ): from beets.ui.commands import config_edit - return config_edit() + return config_edit(options) test_lib = bool(lib) subcommands, plugins, lib = _setup(options, lib) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 99aa04f0a..66faa0b6e 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -2351,7 +2351,10 @@ def config_func(lib, opts, args): # Open in editor. elif opts.edit: - config_edit() + # Note: This branch *should* be unreachable + # since the normal flow should be short-circuited + # by the special case in ui._raw_main + config_edit(opts) # Dump configuration. else: @@ -2362,11 +2365,11 @@ def config_func(lib, opts, args): print("Empty configuration") -def config_edit(): +def config_edit(cli_options): """Open a program to edit the user configuration. An empty config file is created if no existing config file exists. """ - path = config.user_config_path() + path = cli_options.config or config.user_config_path() editor = util.editor_command() try: if not os.path.isfile(path): diff --git a/docs/changelog.rst b/docs/changelog.rst index 88d87e32f..401640f61 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -49,6 +49,9 @@ Bug fixes: * :ref:`query-sort`: Fix a bug that would raise an exception when sorting on a non-string field that is not populated in all items. :bug:`5512` +* Running `beet --config <mypath> config -e` now edits `<mypath>` rather than + the default config path. + :bug:`5652` * :doc:`plugins/lastgenre`: Fix track-level genre handling. Now when an album-level genre is set already, single tracks don't fall back to the album's genre and request their own last.fm genre. Also log messages regarding what's been diff --git a/test/test_config_command.py b/test/test_config_command.py index b68c4f042..c81b143ec 100644 --- a/test/test_config_command.py +++ b/test/test_config_command.py @@ -128,3 +128,15 @@ class ConfigCommandTest(BeetsTestCase): with patch("os.execlp") as execlp: self.run_command("config", "-e") execlp.assert_called_once_with("myeditor", "myeditor", self.config_path) + + def test_edit_config_with_custom_config_path(self): + alt_config_path = os.path.join( + self.temp_dir.decode(), "alt_config.yaml" + ) + with open(self.config_path, "w") as file: + file.write("option: alt value\n") + + os.environ["EDITOR"] = "myeditor" + with patch("os.execlp") as execlp: + self.run_command("--config", alt_config_path, "config", "-e") + execlp.assert_called_once_with("myeditor", "myeditor", alt_config_path) From ecdff785f79f4bca04bcbfcf055e61d892126bf5 Mon Sep 17 00:00:00 2001 From: Aidan Epstein <aidan@jmad.org> Date: Sun, 4 May 2025 00:34:37 -0700 Subject: [PATCH 007/728] Only output verbose details for parentwork plugin when running explicitly (#5135) Fixes #4120. --- beetsplug/parentwork.py | 33 +++++++++++++++++---------------- docs/changelog.rst | 1 + 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 26f8f224f..463a455f5 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -89,7 +89,7 @@ class ParentWorkPlugin(BeetsPlugin): write = ui.should_write() for item in lib.items(ui.decargs(args)): - changed = self.find_work(item, force_parent) + changed = self.find_work(item, force_parent, verbose=True) if changed: item.store() if write: @@ -116,7 +116,7 @@ class ParentWorkPlugin(BeetsPlugin): force_parent = self.config["force"].get(bool) for item in task.imported_items(): - self.find_work(item, force_parent) + self.find_work(item, force_parent, verbose=False) item.store() def get_info(self, item, work_info): @@ -165,7 +165,7 @@ class ParentWorkPlugin(BeetsPlugin): return parentwork_info - def find_work(self, item, force): + def find_work(self, item, force, verbose): """Finds the parent work of a recording and populates the tags accordingly. @@ -221,16 +221,17 @@ add one at https://musicbrainz.org/recording/{}", if work_date: item["work_date"] = work_date - return ui.show_model_changes( - item, - fields=[ - "parentwork", - "parentwork_disambig", - "mb_parentworkid", - "parent_composer", - "parent_composer_sort", - "work_date", - "parentwork_workid_current", - "parentwork_date", - ], - ) + if verbose: + return ui.show_model_changes( + item, + fields=[ + "parentwork", + "parentwork_disambig", + "mb_parentworkid", + "parent_composer", + "parent_composer_sort", + "work_date", + "parentwork_workid_current", + "parentwork_date", + ], + ) diff --git a/docs/changelog.rst b/docs/changelog.rst index e52f329b0..1ab7229e7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -83,6 +83,7 @@ Bug fixes: lyrics. :bug:`5583` * ImageMagick 7.1.1-44 is now supported. +* :doc:`plugins/parentwork`: Only output parentwork changes when running in verbose mode. For packagers: From 52951bf7195fe3130dee92dbd12d6a959e215d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sat, 14 Sep 2024 11:38:40 +0100 Subject: [PATCH 008/728] Fix legalize_path types Background The `_legalize_stage` function was causing issues with Mypy due to inconsistent type usage between the `path` and `extension` parameters. This inconsistency stemmed from the `fragment` parameter influencing the types of these variables. Key issues 1. `path` was defined as `str`, while `extension` was `bytes`. 2. Depending on `fragment`, `extension` could be either `str` or `bytes`. 3. `path` was sometimes converted to `bytes` within `_legalize_stage`. Item.destination` method - The `fragment` parameter determined the output format: - `False`: Returned absolute path as bytes (default) - `True`: Returned path relative to library directory as str Thus - Rename `fragment` parameter to `relative_to_libdir` for clarity - Ensure `Item.destination` returns `bytes` in all cases - Code expecting strings now converts the output to `str` - Use only `str` type in `_legalize_stage` and `_legalize_path` functions - These functions are no longer dependent on `relative_to_libdir` --- beets/library.py | 28 ++++++------ beets/util/__init__.py | 89 +++++++++++++++------------------------ beets/vfs.py | 4 +- beetsplug/bpd/__init__.py | 4 +- beetsplug/convert.py | 8 +--- test/test_library.py | 14 +++--- 6 files changed, 58 insertions(+), 89 deletions(-) diff --git a/beets/library.py b/beets/library.py index d4ec63200..2f5f31393 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1075,19 +1075,17 @@ class Item(LibModel): def destination( self, - fragment=False, + relative_to_libdir=False, basedir=None, platform=None, path_formats=None, replacements=None, - ): - """Return the path in the library directory designated for the - item (i.e., where the file ought to be). + ) -> bytes: + """Return the path in the library directory designated for the item + (i.e., where the file ought to be). - fragment makes this method return just the path fragment underneath - the root library directory; the path is also returned as Unicode - instead of encoded as a bytestring. basedir can override the library's - base directory for the destination. + The path is returned as a bytestring. ``basedir`` can override the + library's base directory for the destination. """ db = self._check_db() platform = platform or sys.platform @@ -1137,14 +1135,13 @@ class Item(LibModel): # When zero, try to determine from filesystem. maxlen = util.max_filename_length(db.directory) - subpath, fellback = util.legalize_path( + lib_path_str, fallback = util.legalize_path( subpath, replacements, maxlen, os.path.splitext(self.path)[1], - fragment, ) - if fellback: + if fallback: # Print an error message if legalization fell back to # default replacements because of the maximum length. log.warning( @@ -1153,11 +1150,12 @@ class Item(LibModel): "the filename.", subpath, ) + lib_path_bytes = util.bytestring_path(lib_path_str) - if fragment: - return util.as_string(subpath) - else: - return normpath(os.path.join(basedir, subpath)) + if relative_to_libdir: + return lib_path_bytes + + return normpath(os.path.join(basedir, lib_path_bytes)) class Album(LibModel): diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 68dbaee65..dc2bb6089 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -712,25 +712,18 @@ def truncate_path(path: AnyStr, length: int = MAX_FILENAME_LENGTH) -> AnyStr: def _legalize_stage( - path: str, - replacements: Replacements | None, - length: int, - extension: str, - fragment: bool, -) -> tuple[BytesOrStr, bool]: + path: str, replacements: Replacements | None, length: int, extension: str +) -> tuple[str, bool]: """Perform a single round of path legalization steps - (sanitation/replacement, encoding from Unicode to bytes, - extension-appending, and truncation). Return the path (Unicode if - `fragment` is set, `bytes` otherwise) and whether truncation was - required. + 1. sanitation/replacement + 2. appending the extension + 3. truncation. + + Return the path and whether truncation was required. """ # Perform an initial sanitization including user replacements. path = sanitize_path(path, replacements) - # Encode for the filesystem. - if not fragment: - path = bytestring_path(path) # type: ignore - # Preserve extension. path += extension.lower() @@ -742,57 +735,41 @@ def _legalize_stage( def legalize_path( - path: str, - replacements: Replacements | None, - length: int, - extension: bytes, - fragment: bool, -) -> tuple[BytesOrStr, bool]: - """Given a path-like Unicode string, produce a legal path. Return - the path and a flag indicating whether some replacements had to be - ignored (see below). + path: str, replacements: Replacements | None, length: int, extension: str +) -> tuple[str, bool]: + """Given a path-like Unicode string, produce a legal path. Return the path + and a flag indicating whether some replacements had to be ignored (see + below). - The legalization process (see `_legalize_stage`) consists of - applying the sanitation rules in `replacements`, encoding the string - to bytes (unless `fragment` is set), truncating components to - `length`, appending the `extension`. + This function uses `_legalize_stage` function to legalize the path, see its + documentation for the details of what this involves. It is called up to + three times in case truncation conflicts with replacements (as can happen + when truncation creates whitespace at the end of the string, for example). - This function performs up to three calls to `_legalize_stage` in - case truncation conflicts with replacements (as can happen when - truncation creates whitespace at the end of the string, for - example). The limited number of iterations iterations avoids the - possibility of an infinite loop of sanitation and truncation - operations, which could be caused by replacement rules that make the - string longer. The flag returned from this function indicates that - the path has to be truncated twice (indicating that replacements - made the string longer again after it was truncated); the - application should probably log some sort of warning. + The limited number of iterations avoids the possibility of an infinite loop + of sanitation and truncation operations, which could be caused by + replacement rules that make the string longer. + + The flag returned from this function indicates that the path has to be + truncated twice (indicating that replacements made the string longer again + after it was truncated); the application should probably log some sort of + warning. """ + args = length, as_string(extension) - if fragment: - # Outputting Unicode. - extension = extension.decode("utf-8", "ignore") - - first_stage_path, _ = _legalize_stage( - path, replacements, length, extension, fragment + first_stage, _ = os.path.splitext( + _legalize_stage(path, replacements, *args)[0] ) - # Convert back to Unicode with extension removed. - first_stage_path, _ = os.path.splitext(displayable_path(first_stage_path)) - # Re-sanitize following truncation (including user replacements). - second_stage_path, retruncated = _legalize_stage( - first_stage_path, replacements, length, extension, fragment - ) + second_stage, truncated = _legalize_stage(first_stage, replacements, *args) - # If the path was once again truncated, discard user replacements + if not truncated: + return second_stage, False + + # If the path was truncated, discard user replacements # and run through one last legalization stage. - if retruncated: - second_stage_path, _ = _legalize_stage( - first_stage_path, None, length, extension, fragment - ) - - return second_stage_path, retruncated + return _legalize_stage(first_stage, None, *args)[0], True def str2bool(value: str) -> bool: diff --git a/beets/vfs.py b/beets/vfs.py index cdbf197a6..4fd133f5a 100644 --- a/beets/vfs.py +++ b/beets/vfs.py @@ -49,7 +49,7 @@ def libtree(lib): """ root = Node({}, {}) for item in lib.items(): - dest = item.destination(fragment=True) - parts = util.components(dest) + dest = item.destination(relative_to_libdir=True) + parts = util.components(util.as_string(dest)) _insert(root, parts, item.id) return root diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index 9d8b4142b..435368e35 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -33,7 +33,7 @@ import beets.ui from beets import dbcore, vfs from beets.library import Item from beets.plugins import BeetsPlugin -from beets.util import bluelet +from beets.util import as_string, bluelet if TYPE_CHECKING: from beets.dbcore.query import Query @@ -1130,7 +1130,7 @@ class Server(BaseServer): def _item_info(self, item): info_lines = [ - "file: " + item.destination(fragment=True), + "file: " + as_string(item.destination(relative_to_libdir=True)), "Time: " + str(int(item.length)), "duration: " + f"{item.length:.3f}", "Id: " + str(item.id), diff --git a/beetsplug/convert.py b/beetsplug/convert.py index a8c32e955..7586c2a1b 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -624,13 +624,7 @@ class ConvertPlugin(BeetsPlugin): # strings we get from item.destination to bytes. items_paths = [ os.path.relpath( - util.bytestring_path( - item.destination( - basedir=dest, - path_formats=path_formats, - fragment=False, - ) - ), + item.destination(basedir=dest, path_formats=path_formats), pl_dir, ) for item in items diff --git a/test/test_library.py b/test/test_library.py index b5e6d4eeb..d6804449f 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -34,7 +34,7 @@ from beets.library import Album from beets.test import _common from beets.test._common import item from beets.test.helper import BeetsTestCase, ItemInDBTestCase -from beets.util import bytestring_path, syspath +from beets.util import as_string, bytestring_path, syspath # Shortcut to path normalization. np = util.normpath @@ -411,14 +411,14 @@ class DestinationTest(BeetsTestCase): def test_unicode_normalized_nfd_on_mac(self): instr = unicodedata.normalize("NFC", "caf\xe9") self.lib.path_formats = [("default", instr)] - dest = self.i.destination(platform="darwin", fragment=True) - assert dest == unicodedata.normalize("NFD", instr) + dest = self.i.destination(platform="darwin", relative_to_libdir=True) + assert as_string(dest) == unicodedata.normalize("NFD", instr) def test_unicode_normalized_nfc_on_linux(self): instr = unicodedata.normalize("NFD", "caf\xe9") self.lib.path_formats = [("default", instr)] - dest = self.i.destination(platform="linux", fragment=True) - assert dest == unicodedata.normalize("NFC", instr) + dest = self.i.destination(platform="linux", relative_to_libdir=True) + assert as_string(dest) == unicodedata.normalize("NFC", instr) def test_non_mbcs_characters_on_windows(self): oldfunc = sys.getfilesystemencoding @@ -436,8 +436,8 @@ class DestinationTest(BeetsTestCase): def test_unicode_extension_in_fragment(self): self.lib.path_formats = [("default", "foo")] self.i.path = util.bytestring_path("bar.caf\xe9") - dest = self.i.destination(platform="linux", fragment=True) - assert dest == "foo.caf\xe9" + dest = self.i.destination(platform="linux", relative_to_libdir=True) + assert as_string(dest) == "foo.caf\xe9" def test_asciify_and_replace(self): config["asciify_paths"] = True From 06dd2738bb3dbca40dfe2b2a80786d594cddced2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 15 Sep 2024 03:13:42 +0100 Subject: [PATCH 009/728] Remove never used replacements paramater from item.destination --- beets/library.py | 8 +------- test/test_library.py | 11 ----------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/beets/library.py b/beets/library.py index 2f5f31393..f45e63ebe 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1079,7 +1079,6 @@ class Item(LibModel): basedir=None, platform=None, path_formats=None, - replacements=None, ) -> bytes: """Return the path in the library directory designated for the item (i.e., where the file ought to be). @@ -1091,8 +1090,6 @@ class Item(LibModel): platform = platform or sys.platform basedir = basedir or db.directory path_formats = path_formats or db.path_formats - if replacements is None: - replacements = self._db.replacements # Use a path format based on a query, falling back on the # default. @@ -1136,10 +1133,7 @@ class Item(LibModel): maxlen = util.max_filename_length(db.directory) lib_path_str, fallback = util.legalize_path( - subpath, - replacements, - maxlen, - os.path.splitext(self.path)[1], + subpath, db.replacements, maxlen, os.path.splitext(self.path)[1] ) if fallback: # Print an error message if legalization fell back to diff --git a/test/test_library.py b/test/test_library.py index d6804449f..342c2fe20 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -462,17 +462,6 @@ class DestinationTest(BeetsTestCase): self.i.album = "bar" assert self.i.destination() == np("base/ber/foo") - def test_destination_with_replacements_argument(self): - self.lib.directory = b"base" - self.lib.replacements = [(re.compile(r"a"), "f")] - self.lib.path_formats = [("default", "$album/$title")] - self.i.title = "foo" - self.i.album = "bar" - replacements = [(re.compile(r"a"), "e")] - assert self.i.destination(replacements=replacements) == np( - "base/ber/foo" - ) - @unittest.skip("unimplemented: #359") def test_destination_with_empty_component(self): self.lib.directory = b"base" From d3186859acfa1cbe771eb43a9e9d720f8066590a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 15 Sep 2024 03:15:14 +0100 Subject: [PATCH 010/728] Remove unused platform parameter in item.destination --- beets/library.py | 4 +--- test/test_library.py | 10 +++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/beets/library.py b/beets/library.py index f45e63ebe..ec530a5dd 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1077,7 +1077,6 @@ class Item(LibModel): self, relative_to_libdir=False, basedir=None, - platform=None, path_formats=None, ) -> bytes: """Return the path in the library directory designated for the item @@ -1087,7 +1086,6 @@ class Item(LibModel): library's base directory for the destination. """ db = self._check_db() - platform = platform or sys.platform basedir = basedir or db.directory path_formats = path_formats or db.path_formats @@ -1117,7 +1115,7 @@ class Item(LibModel): subpath = self.evaluate_template(subpath_tmpl, True) # Prepare path for output: normalize Unicode characters. - if platform == "darwin": + if sys.platform == "darwin": subpath = unicodedata.normalize("NFD", subpath) else: subpath = unicodedata.normalize("NFC", subpath) diff --git a/test/test_library.py b/test/test_library.py index 342c2fe20..a4e6dab44 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -23,6 +23,7 @@ import sys import time import unicodedata import unittest +from unittest.mock import patch import pytest from mediafile import MediaFile, UnreadableFileError @@ -411,13 +412,15 @@ class DestinationTest(BeetsTestCase): def test_unicode_normalized_nfd_on_mac(self): instr = unicodedata.normalize("NFC", "caf\xe9") self.lib.path_formats = [("default", instr)] - dest = self.i.destination(platform="darwin", relative_to_libdir=True) + with patch("sys.platform", "darwin"): + dest = self.i.destination(relative_to_libdir=True) assert as_string(dest) == unicodedata.normalize("NFD", instr) def test_unicode_normalized_nfc_on_linux(self): instr = unicodedata.normalize("NFD", "caf\xe9") self.lib.path_formats = [("default", instr)] - dest = self.i.destination(platform="linux", relative_to_libdir=True) + with patch("sys.platform", "linux"): + dest = self.i.destination(relative_to_libdir=True) assert as_string(dest) == unicodedata.normalize("NFC", instr) def test_non_mbcs_characters_on_windows(self): @@ -436,7 +439,8 @@ class DestinationTest(BeetsTestCase): def test_unicode_extension_in_fragment(self): self.lib.path_formats = [("default", "foo")] self.i.path = util.bytestring_path("bar.caf\xe9") - dest = self.i.destination(platform="linux", relative_to_libdir=True) + with patch("sys.platform", "linux"): + dest = self.i.destination(relative_to_libdir=True) assert as_string(dest) == "foo.caf\xe9" def test_asciify_and_replace(self): From b3fd84b35651edf2c7b7958a06debec4d3384558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 10 Mar 2025 00:07:21 +0000 Subject: [PATCH 011/728] Move max filename length calculation closer to where it is used --- beets/library.py | 7 +------ beets/util/__init__.py | 30 +++++++++++++++++++----------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/beets/library.py b/beets/library.py index ec530a5dd..1fe253c18 100644 --- a/beets/library.py +++ b/beets/library.py @@ -1125,13 +1125,8 @@ class Item(LibModel): subpath, beets.config["path_sep_replace"].as_str() ) - maxlen = beets.config["max_filename_length"].get(int) - if not maxlen: - # When zero, try to determine from filesystem. - maxlen = util.max_filename_length(db.directory) - lib_path_str, fallback = util.legalize_path( - subpath, db.replacements, maxlen, os.path.splitext(self.path)[1] + subpath, db.replacements, os.path.splitext(self.path)[1] ) if fallback: # Print an error message if legalization fell back to diff --git a/beets/util/__init__.py b/beets/util/__init__.py index dc2bb6089..d8340a978 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -30,6 +30,7 @@ import traceback from collections import Counter from contextlib import suppress from enum import Enum +from functools import cache from importlib import import_module from multiprocessing.pool import ThreadPool from pathlib import Path @@ -47,6 +48,7 @@ from typing import ( from unidecode import unidecode +import beets from beets.util import hidden if TYPE_CHECKING: @@ -694,25 +696,26 @@ def sanitize_path(path: str, replacements: Replacements | None = None) -> str: return os.path.join(*comps) -def truncate_path(path: AnyStr, length: int = MAX_FILENAME_LENGTH) -> AnyStr: +def truncate_path(path: AnyStr) -> AnyStr: """Given a bytestring path or a Unicode path fragment, truncate the components to a legal length. In the last component, the extension is preserved. """ + max_length = get_max_filename_length() comps = components(path) out = [c[:length] for c in comps] base, ext = os.path.splitext(comps[-1]) if ext: # Last component has an extension. - base = base[: length - len(ext)] + base = base[: max_length - len(ext)] out[-1] = base + ext return os.path.join(*out) def _legalize_stage( - path: str, replacements: Replacements | None, length: int, extension: str + path: str, replacements: Replacements | None, extension: str ) -> tuple[str, bool]: """Perform a single round of path legalization steps 1. sanitation/replacement @@ -729,13 +732,13 @@ def _legalize_stage( # Truncate too-long components. pre_truncate_path = path - path = truncate_path(path, length) + path = truncate_path(path) return path, path != pre_truncate_path def legalize_path( - path: str, replacements: Replacements | None, length: int, extension: str + path: str, replacements: Replacements | None, extension: str ) -> tuple[str, bool]: """Given a path-like Unicode string, produce a legal path. Return the path and a flag indicating whether some replacements had to be ignored (see @@ -755,21 +758,21 @@ def legalize_path( after it was truncated); the application should probably log some sort of warning. """ - args = length, as_string(extension) + suffix = as_string(extension) first_stage, _ = os.path.splitext( - _legalize_stage(path, replacements, *args)[0] + _legalize_stage(path, replacements, suffix)[0] ) # Re-sanitize following truncation (including user replacements). - second_stage, truncated = _legalize_stage(first_stage, replacements, *args) + second_stage, truncated = _legalize_stage(first_stage, replacements, suffix) if not truncated: return second_stage, False # If the path was truncated, discard user replacements # and run through one last legalization stage. - return _legalize_stage(first_stage, None, *args)[0], True + return _legalize_stage(first_stage, None, suffix)[0], True def str2bool(value: str) -> bool: @@ -848,16 +851,21 @@ def command_output(cmd: list[BytesOrStr], shell: bool = False) -> CommandOutput: return CommandOutput(stdout, stderr) -def max_filename_length(path: BytesOrStr, limit=MAX_FILENAME_LENGTH) -> int: +@cache +def get_max_filename_length() -> int: """Attempt to determine the maximum filename length for the filesystem containing `path`. If the value is greater than `limit`, then `limit` is used instead (to prevent errors when a filesystem misreports its capacity). If it cannot be determined (e.g., on Windows), return `limit`. """ + if length := beets.config["max_filename_length"].get(int): + return length + + limit = MAX_FILENAME_LENGTH if hasattr(os, "statvfs"): try: - res = os.statvfs(path) + res = os.statvfs(beets.config["directory"].as_str()) except OSError: return limit return min(res[9], limit) From 40fbc8ee7e9941c5bd8487a24370f6b734dcf451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 10 Mar 2025 03:16:37 +0000 Subject: [PATCH 012/728] Fix and simplify path truncation --- beets/util/__init__.py | 32 ++++++++++++++++++-------------- test/test_util.py | 27 ++++++++++++--------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index d8340a978..ff0a5d273 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -696,22 +696,26 @@ def sanitize_path(path: str, replacements: Replacements | None = None) -> str: return os.path.join(*comps) -def truncate_path(path: AnyStr) -> AnyStr: - """Given a bytestring path or a Unicode path fragment, truncate the - components to a legal length. In the last component, the extension - is preserved. +def truncate_str(s: str, length: int) -> str: + """Truncate the string to the given byte length. + + If we end up truncating a unicode character in the middle (rendering it invalid), + it is removed: + + >>> s = "🎹🎶" # 8 bytes + >>> truncate_str(s, 6) + '🎹' """ + return os.fsencode(s)[:length].decode(sys.getfilesystemencoding(), "ignore") + + +def truncate_path(str_path: str) -> str: + """Truncate each path part to a legal length preserving the extension.""" max_length = get_max_filename_length() - comps = components(path) - - out = [c[:length] for c in comps] - base, ext = os.path.splitext(comps[-1]) - if ext: - # Last component has an extension. - base = base[: max_length - len(ext)] - out[-1] = base + ext - - return os.path.join(*out) + path = Path(str_path) + parent_parts = [truncate_str(p, max_length) for p in path.parts[:-1]] + stem = truncate_str(path.stem, max_length - len(path.suffix)) + return str(Path(*parent_parts, stem).with_suffix(path.suffix)) def _legalize_stage( diff --git a/test/test_util.py b/test/test_util.py index f5b4fd102..a4b224ee3 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -175,18 +175,15 @@ class PathConversionTest(BeetsTestCase): assert outpath == "C:\\caf\xe9".encode() -class PathTruncationTest(BeetsTestCase): - def test_truncate_bytestring(self): - with _common.platform_posix(): - p = util.truncate_path(b"abcde/fgh", 4) - assert p == b"abcd/fgh" - - def test_truncate_unicode(self): - with _common.platform_posix(): - p = util.truncate_path("abcde/fgh", 4) - assert p == "abcd/fgh" - - def test_truncate_preserves_extension(self): - with _common.platform_posix(): - p = util.truncate_path("abcde/fgh.ext", 5) - assert p == "abcde/f.ext" +@patch("beets.util.get_max_filename_length", lambda: 5) +@pytest.mark.parametrize( + "path, expected", + [ + ("abcdeX/fgh", "abcde/fgh"), + ("abcde/fXX.ext", "abcde/f.ext"), + ("a🎹/a.ext", "a🎹/a.ext"), + ("ab🎹/a.ext", "ab/a.ext"), + ], +) +def test_truncate_path(path, expected): + assert util.truncate_path(path) == expected From 4fcb148d602cb8378e26b1e044a43587133c33e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Thu, 13 Mar 2025 08:18:31 +0000 Subject: [PATCH 013/728] Add test for legalization logic --- test/test_library.py | 31 --------------- test/test_util.py | 92 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 80 insertions(+), 43 deletions(-) diff --git a/test/test_library.py b/test/test_library.py index a4e6dab44..d90b9efd7 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -487,37 +487,6 @@ class DestinationTest(BeetsTestCase): self.i.path = "foo.mp3" assert self.i.destination() == np("base/one/_.mp3") - def test_legalize_path_one_for_one_replacement(self): - # Use a replacement that should always replace the last X in any - # path component with a Z. - self.lib.replacements = [ - (re.compile(r"X$"), "Z"), - ] - - # Construct an item whose untruncated path ends with a Y but whose - # truncated version ends with an X. - self.i.title = "X" * 300 + "Y" - - # The final path should reflect the replacement. - dest = self.i.destination() - assert dest[-2:] == b"XZ" - - def test_legalize_path_one_for_many_replacement(self): - # Use a replacement that should always replace the last X in any - # path component with four Zs. - self.lib.replacements = [ - (re.compile(r"X$"), "ZZZZ"), - ] - - # Construct an item whose untruncated path ends with a Y but whose - # truncated version ends with an X. - self.i.title = "X" * 300 + "Y" - - # The final path should ignore the user replacement and create a path - # of the correct length, containing Xs. - dest = self.i.destination() - assert dest[-2:] == b"XX" - def test_album_field_query(self): self.lib.directory = b"one" self.lib.path_formats = [("default", "two"), ("flex:foo", "three")] diff --git a/test/test_util.py b/test/test_util.py index a4b224ee3..b82ebed10 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -175,15 +175,83 @@ class PathConversionTest(BeetsTestCase): assert outpath == "C:\\caf\xe9".encode() -@patch("beets.util.get_max_filename_length", lambda: 5) -@pytest.mark.parametrize( - "path, expected", - [ - ("abcdeX/fgh", "abcde/fgh"), - ("abcde/fXX.ext", "abcde/f.ext"), - ("a🎹/a.ext", "a🎹/a.ext"), - ("ab🎹/a.ext", "ab/a.ext"), - ], -) -def test_truncate_path(path, expected): - assert util.truncate_path(path) == expected +class TestPathLegalization: + @pytest.fixture(autouse=True) + def _patch_max_filename_length(self, monkeypatch): + monkeypatch.setattr("beets.util.get_max_filename_length", lambda: 5) + + @pytest.mark.parametrize( + "path, expected", + [ + ("abcdeX/fgh", "abcde/fgh"), + ("abcde/fXX.ext", "abcde/f.ext"), + ("a🎹/a.ext", "a🎹/a.ext"), + ("ab🎹/a.ext", "ab/a.ext"), + ], + ) + def test_truncate(self, path, expected): + assert util.truncate_path(path) == expected + + @pytest.mark.parametrize( + "pre_trunc_repl, post_trunc_repl, expected", + [ + pytest.param( + [], + [], + ("_abcd", False), + id="default", + ), + pytest.param( + [(re.compile(r"abcdX$"), "PRE")], + [], + (":PRE", False), + id="valid path after initial repl", + ), + pytest.param( + [(re.compile(r"abcdX$"), "PRE_LONG")], + [], + (":PRE_", False), + id="too long path after initial repl is truncated", + ), + pytest.param( + [], + [(re.compile(r"abcdX$"), "POST")], + (":POST", False), + id="valid path after post-trunc repl", + ), + pytest.param( + [], + [(re.compile(r"abcdX$"), "POST_LONG")], + (":POST", False), + id="too long path after post-trunc repl is truncated", + ), + pytest.param( + [(re.compile(r"abcdX$"), "PRE")], + [(re.compile(r"PRE$"), "POST")], + (":POST", False), + id="both replacements within filename length limit", + ), + pytest.param( + [(re.compile(r"abcdX$"), "PRE_LONG")], + [(re.compile(r"PRE_$"), "POST")], + (":POST", False), + id="too long initial path is truncated and valid post-trunc repl", + ), + pytest.param( + [(re.compile(r"abcdX$"), "PRE")], + [(re.compile(r"PRE$"), "POST_LONG")], + (":POST", False), + id="valid pre-trunc repl and too long post-trunc path is truncated", + ), + pytest.param( + [(re.compile(r"abcdX$"), "PRE_LONG")], + [(re.compile(r"PRE_$"), "POST_LONG")], + ("_PRE_", True), + id="too long repl both times force default ones to be applied", + ), + ], + ) + def test_replacements(self, pre_trunc_repl, post_trunc_repl, expected): + replacements = pre_trunc_repl + post_trunc_repl + + assert util.legalize_path(":abcdX", replacements, "") == expected From 5826d6b59bd7cc082254080085a61c8ea44b0458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 20 Apr 2025 10:24:57 +0100 Subject: [PATCH 014/728] Remove handling of mbcs encoding This has been phased out in Python 3.6. https://peps.python.org/pep-0529/ --- test/test_library.py | 14 -------------- test/test_util.py | 9 ++------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/test/test_library.py b/test/test_library.py index d90b9efd7..8e5e01f5c 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -19,7 +19,6 @@ import os.path import re import shutil import stat -import sys import time import unicodedata import unittest @@ -423,19 +422,6 @@ class DestinationTest(BeetsTestCase): dest = self.i.destination(relative_to_libdir=True) assert as_string(dest) == unicodedata.normalize("NFC", instr) - def test_non_mbcs_characters_on_windows(self): - oldfunc = sys.getfilesystemencoding - sys.getfilesystemencoding = lambda: "mbcs" - try: - self.i.title = "h\u0259d" - self.lib.path_formats = [("default", "$title")] - p = self.i.destination() - assert b"?" not in p - # We use UTF-8 to encode Windows paths now. - assert "h\u0259d".encode() in p - finally: - sys.getfilesystemencoding = oldfunc - def test_unicode_extension_in_fragment(self): self.lib.path_formats = [("default", "foo")] self.i.path = util.bytestring_path("bar.caf\xe9") diff --git a/test/test_util.py b/test/test_util.py index b82ebed10..28a3a4ce1 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -156,13 +156,8 @@ class PathConversionTest(BeetsTestCase): assert path == outpath def _windows_bytestring_path(self, path): - old_gfse = sys.getfilesystemencoding - sys.getfilesystemencoding = lambda: "mbcs" - try: - with _common.platform_windows(): - return util.bytestring_path(path) - finally: - sys.getfilesystemencoding = old_gfse + with _common.platform_windows(): + return util.bytestring_path(path) def test_bytestring_path_windows_encodes_utf8(self): path = "caf\xe9" From 6a7832f207a51d922380b1ccc23eb85ad692a16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 20 Apr 2025 15:01:15 +0100 Subject: [PATCH 015/728] Adjust tests to work in Windows --- test/test_library.py | 9 +++++++-- test/test_util.py | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/test_library.py b/test/test_library.py index 8e5e01f5c..39f1d0b9e 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -559,8 +559,13 @@ class PathFormattingMixin: def _assert_dest(self, dest, i=None): if i is None: i = self.i - with _common.platform_posix(): - actual = i.destination() + + if os.path.sep != "/": + dest = dest.replace(b"/", os.path.sep.encode()) + dest = b"D:" + dest + + actual = i.destination() + assert actual == dest diff --git a/test/test_util.py b/test/test_util.py index 28a3a4ce1..403071df2 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -185,6 +185,9 @@ class TestPathLegalization: ], ) def test_truncate(self, path, expected): + path = path.replace("/", os.path.sep) + expected = expected.replace("/", os.path.sep) + assert util.truncate_path(path) == expected @pytest.mark.parametrize( From 921b7ed9ea572a664accf45462f68101028215d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 27 Apr 2025 14:07:32 +0100 Subject: [PATCH 016/728] Rewrite legalisation tests for readability --- pyproject.toml | 1 + test/test_util.py | 85 +++++++++++++---------------------------------- 2 files changed, 24 insertions(+), 62 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d985c54ea..6819d6624 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -263,6 +263,7 @@ select = [ ] [tool.ruff.lint.per-file-ignores] "beets/**" = ["PT"] +"test/test_util.py" = ["E501"] [tool.ruff.lint.isort] split-on-trailing-comma = false diff --git a/test/test_util.py b/test/test_util.py index 403071df2..3a5e55c49 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -190,66 +190,27 @@ class TestPathLegalization: assert util.truncate_path(path) == expected - @pytest.mark.parametrize( - "pre_trunc_repl, post_trunc_repl, expected", - [ - pytest.param( - [], - [], - ("_abcd", False), - id="default", - ), - pytest.param( - [(re.compile(r"abcdX$"), "PRE")], - [], - (":PRE", False), - id="valid path after initial repl", - ), - pytest.param( - [(re.compile(r"abcdX$"), "PRE_LONG")], - [], - (":PRE_", False), - id="too long path after initial repl is truncated", - ), - pytest.param( - [], - [(re.compile(r"abcdX$"), "POST")], - (":POST", False), - id="valid path after post-trunc repl", - ), - pytest.param( - [], - [(re.compile(r"abcdX$"), "POST_LONG")], - (":POST", False), - id="too long path after post-trunc repl is truncated", - ), - pytest.param( - [(re.compile(r"abcdX$"), "PRE")], - [(re.compile(r"PRE$"), "POST")], - (":POST", False), - id="both replacements within filename length limit", - ), - pytest.param( - [(re.compile(r"abcdX$"), "PRE_LONG")], - [(re.compile(r"PRE_$"), "POST")], - (":POST", False), - id="too long initial path is truncated and valid post-trunc repl", - ), - pytest.param( - [(re.compile(r"abcdX$"), "PRE")], - [(re.compile(r"PRE$"), "POST_LONG")], - (":POST", False), - id="valid pre-trunc repl and too long post-trunc path is truncated", - ), - pytest.param( - [(re.compile(r"abcdX$"), "PRE_LONG")], - [(re.compile(r"PRE_$"), "POST_LONG")], - ("_PRE_", True), - id="too long repl both times force default ones to be applied", - ), - ], - ) - def test_replacements(self, pre_trunc_repl, post_trunc_repl, expected): - replacements = pre_trunc_repl + post_trunc_repl + _p = pytest.param - assert util.legalize_path(":abcdX", replacements, "") == expected + @pytest.mark.parametrize( + "replacements, expected_path, expected_truncated", + [ # [ repl before truncation, repl after truncation ] + _p([ ], "_abcd", False, id="default"), + _p([(r"abcdX$", "1ST"), ], ":1ST", False, id="1st_valid"), + _p([(r"abcdX$", "TOO_LONG"), ], ":TOO_", False, id="1st_truncated"), + _p([(r"abcdX$", "1ST"), (r"1ST$", "2ND") ], ":2ND", False, id="both_valid"), + _p([(r"abcdX$", "TOO_LONG"), (r"TOO_$", "2ND") ], ":2ND", False, id="1st_truncated_2nd_valid"), + _p([(r"abcdX$", "1ST"), (r"1ST$", "TOO_LONG") ], ":TOO_", False, id="1st_valid_2nd_truncated"), + # if the logic truncates the path twice, it ends up applying the default replacements + _p([(r"abcdX$", "TOO_LONG"), (r"TOO_$", "TOO_LONG") ], "_TOO_", True, id="both_truncated_default_repl_applied"), + ] + ) # fmt: skip + def test_replacements( + self, replacements, expected_path, expected_truncated + ): + replacements = [(re.compile(pat), repl) for pat, repl in replacements] + + assert util.legalize_path(":abcdX", replacements, "") == ( + expected_path, + expected_truncated, + ) From 2c683493149d2c4c826f9b9864a996a97264a243 Mon Sep 17 00:00:00 2001 From: wisp3rwind <17089248+wisp3rwind@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:19:40 +0200 Subject: [PATCH 017/728] pipeline: remove old tests, integrate with out test suite These have probably not been run by anyone in ages, better to move the code to our test suite where it is regularly exercised. In fact, the latter covers most of the cases already. The only missing tests seem to be those were exceptions are raised in the first or last stage. Thus, this adds such tests. --- beets/util/pipeline.py | 61 ------------------------------------------ test/test_pipeline.py | 44 ++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 69 deletions(-) diff --git a/beets/util/pipeline.py b/beets/util/pipeline.py index 98a1addce..cebde0f23 100644 --- a/beets/util/pipeline.py +++ b/beets/util/pipeline.py @@ -492,64 +492,3 @@ class Pipeline: msgs = next_msgs for msg in msgs: yield msg - - -# Smoke test. -if __name__ == "__main__": - import time - - # Test a normally-terminating pipeline both in sequence and - # in parallel. - def produce(): - for i in range(5): - print("generating %i" % i) - time.sleep(1) - yield i - - def work(): - num = yield - while True: - print("processing %i" % num) - time.sleep(2) - num = yield num * 2 - - def consume(): - while True: - num = yield - time.sleep(1) - print("received %i" % num) - - ts_start = time.time() - Pipeline([produce(), work(), consume()]).run_sequential() - ts_seq = time.time() - Pipeline([produce(), work(), consume()]).run_parallel() - ts_par = time.time() - Pipeline([produce(), (work(), work()), consume()]).run_parallel() - ts_end = time.time() - print("Sequential time:", ts_seq - ts_start) - print("Parallel time:", ts_par - ts_seq) - print("Multiply-parallel time:", ts_end - ts_par) - print() - - # Test a pipeline that raises an exception. - def exc_produce(): - for i in range(10): - print("generating %i" % i) - time.sleep(1) - yield i - - def exc_work(): - num = yield - while True: - print("processing %i" % num) - time.sleep(3) - if num == 3: - raise Exception() - num = yield num * 2 - - def exc_consume(): - while True: - num = yield - print("received %i" % num) - - Pipeline([exc_produce(), exc_work(), exc_consume()]).run_parallel(1) diff --git a/test/test_pipeline.py b/test/test_pipeline.py index 83b8d744c..5007ad826 100644 --- a/test/test_pipeline.py +++ b/test/test_pipeline.py @@ -39,11 +39,16 @@ def _consume(result): result.append(i) -# A worker that raises an exception. +# Pipeline stages that raise an exception. class PipelineError(Exception): pass +def _exc_produce(num=5): + yield from range(num) + raise PipelineError() + + def _exc_work(num=3): i = None while True: @@ -53,6 +58,14 @@ def _exc_work(num=3): i *= 2 +def _exc_consume(result, num=4): + while True: + i = yield + if i == num: + raise PipelineError() + result.append(i) + + # A worker that yields a bubble. def _bub_work(num=3): i = None @@ -121,17 +134,32 @@ class ParallelStageTest(unittest.TestCase): class ExceptionTest(unittest.TestCase): def setUp(self): self.result = [] - self.pl = pipeline.Pipeline( - (_produce(), _exc_work(), _consume(self.result)) - ) + + def run_sequential(self, *stages): + pl = pipeline.Pipeline(stages) + with pytest.raises(PipelineError): + pl.run_sequential() + + def run_parallel(self, *stages): + pl = pipeline.Pipeline(stages) + with pytest.raises(PipelineError): + pl.run_parallel() def test_run_sequential(self): - with pytest.raises(PipelineError): - self.pl.run_sequential() + """Test that exceptions from various stages of the pipeline are + properly propagated when running sequentially. + """ + self.run_sequential(_exc_produce(), _work(), _consume(self.result)) + self.run_sequential(_produce(), _exc_work(), _consume(self.result)) + self.run_sequential(_produce(), _work(), _exc_consume(self.result)) def test_run_parallel(self): - with pytest.raises(PipelineError): - self.pl.run_parallel() + """Test that exceptions from various stages of the pipeline are + properly propagated when running in parallel. + """ + self.run_parallel(_exc_produce(), _work(), _consume(self.result)) + self.run_parallel(_produce(), _exc_work(), _consume(self.result)) + self.run_parallel(_produce(), _work(), _exc_consume(self.result)) def test_pull(self): pl = pipeline.Pipeline((_produce(), _exc_work())) From a40a3d45e4dc5fd80ee409195a95f494f46c7c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 10:24:02 +0100 Subject: [PATCH 018/728] Install docs dependencies early --- .github/workflows/make_release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/make_release.yaml b/.github/workflows/make_release.yaml index 248755703..3fc1598d6 100644 --- a/.github/workflows/make_release.yaml +++ b/.github/workflows/make_release.yaml @@ -26,7 +26,7 @@ jobs: cache: poetry - name: Install dependencies - run: poetry install --only=release + run: poetry install --with=release --extras=docs - name: Bump project version run: poe bump "${{ env.NEW_VERSION }}" From 5128a817be8a8e2878fa9576f387cff6fc6e15db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 01:03:23 +0100 Subject: [PATCH 019/728] Update missed out python version in the build --- .github/workflows/integration_test.yaml | 2 +- .github/workflows/make_release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 88945bb8e..1a848bde5 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -12,7 +12,7 @@ jobs: uses: BrandonLWhite/pipx-install-action@v0.1.1 - uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 cache: poetry - name: Install dependencies diff --git a/.github/workflows/make_release.yaml b/.github/workflows/make_release.yaml index 3fc1598d6..e54381392 100644 --- a/.github/workflows/make_release.yaml +++ b/.github/workflows/make_release.yaml @@ -8,7 +8,7 @@ on: required: true env: - PYTHON_VERSION: 3.8 + PYTHON_VERSION: 3.9 NEW_VERSION: ${{ inputs.version }} NEW_TAG: v${{ inputs.version }} From 63c23c32ed6c5df35c84de50ebc3df688f57ed9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 01:03:56 +0100 Subject: [PATCH 020/728] Add missed out python versions to package classifiers --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6819d6624..8f4403f20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,8 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", ] packages = [ From 16a6cb1340b9a892689938edeeb764580992bef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 05:07:41 +0100 Subject: [PATCH 021/728] Update dependencies And thus address the following security vulnerabilities: https://github.com/beetbox/beets/security/dependabot --- .github/workflows/ci.yaml | 2 +- poetry.lock | 2065 ++++++++++++++++++++----------------- 2 files changed, 1099 insertions(+), 968 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ea79d59b2..a6720335f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt update - sudo apt install ffmpeg gobject-introspection libcairo2-dev libgirepository1.0-dev pandoc + sudo apt install ffmpeg gobject-introspection libcairo2-dev libgirepository-2.0-dev pandoc - name: Get changed lyrics files id: lyrics-update diff --git a/poetry.lock b/poetry.lock index 8fa603a13..fe9dec791 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -6,6 +6,8 @@ version = "0.0.5" description = "A collection of accessible pygments styles" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, @@ -24,6 +26,8 @@ version = "0.7.16" description = "A light, configurable Sphinx theme" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, @@ -31,24 +35,25 @@ files = [ [[package]] name = "anyio" -version = "4.6.2.post1" +version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -57,6 +62,8 @@ version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"sonosupdate\"" files = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -68,6 +75,8 @@ version = "3.0.1" description = "Multi-library, cross-platform audio decoding." optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"autobpm\" or extra == \"chroma\"" files = [ {file = "audioread-3.0.1-py3-none-any.whl", hash = "sha256:4cdce70b8adc0da0a3c9e0d85fb10b3ace30fbdf8d1670fd443929b61d117c33"}, {file = "audioread-3.0.1.tar.gz", hash = "sha256:ac5460a5498c48bdf2e8e767402583a4dcd13f4414d286f42ce4379e8b35066d"}, @@ -78,31 +87,35 @@ test = ["tox"] [[package]] name = "babel" -version = "2.16.0" +version = "2.17.0" description = "Internationalization utilities" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"docs\"" files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "beautifulsoup4" -version = "4.12.3" +version = "4.13.4" description = "Screen-scraping library" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.7.0" +groups = ["main", "test"] files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, + {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, + {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, ] [package.dependencies] soupsieve = ">1.2" +typing-extensions = ">=4.0.0" [package.extras] cchardet = ["cchardet"] @@ -117,6 +130,7 @@ version = "1.9.0" description = "Fast, simple object-to-object and broadcast signaling" optional = false python-versions = ">=3.9" +groups = ["main", "test", "typing"] files = [ {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, @@ -128,6 +142,8 @@ version = "1.1.0" description = "Python bindings for the Brotli compression library" optional = false python-versions = "*" +groups = ["main", "test"] +markers = "platform_python_implementation == \"CPython\"" files = [ {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, @@ -262,6 +278,8 @@ version = "1.1.0.0" description = "Python CFFI bindings to the Brotli library" optional = false python-versions = ">=3.7" +groups = ["main", "test"] +markers = "platform_python_implementation == \"PyPy\"" files = [ {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, @@ -297,13 +315,14 @@ cffi = ">=1.0.0" [[package]] name = "certifi" -version = "2024.8.30" +version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "test"] files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, + {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, ] [[package]] @@ -312,6 +331,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -381,133 +401,123 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] +markers = {main = "extra == \"autobpm\" or extra == \"reflink\" or platform_python_implementation == \"PyPy\"", test = "platform_python_implementation == \"PyPy\""} [package.dependencies] pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" +groups = ["main", "test"] files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "release", "test", "typing"] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -519,6 +529,7 @@ version = "2.1.13" description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["test"] files = [ {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"}, {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"}, @@ -534,6 +545,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "release", "test", "typing"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -545,6 +558,7 @@ version = "2.0.1" description = "Painless YAML configuration." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "confuse-2.0.1-py3-none-any.whl", hash = "sha256:9b9e5bbc70e2cb9b318bcab14d917ec88e21bf1b724365e3815eb16e37aabd2a"}, {file = "confuse-2.0.1.tar.gz", hash = "sha256:7379a2ad49aaa862b79600cc070260c1b7974d349f4fa5e01f9afa6c4dd0611f"}, @@ -555,89 +569,93 @@ pyyaml = "*" [[package]] name = "coverage" -version = "7.6.8" +version = "7.8.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, - {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, - {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, - {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, - {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, - {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, - {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, - {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, - {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, - {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, - {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, - {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, - {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, - {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, - {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, - {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, - {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, - {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, - {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, - {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, - {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, - {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, - {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, - {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, - {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, - {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "dbus-python" -version = "1.3.2" +version = "1.4.0" description = "Python bindings for libdbus" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"metasync\"" files = [ - {file = "dbus-python-1.3.2.tar.gz", hash = "sha256:ad67819308618b5069537be237f8e68ca1c7fcc95ee4a121fe6845b1418248f8"}, + {file = "dbus-python-1.4.0.tar.gz", hash = "sha256:991666e498f60dbf3e49b8b7678f5559b8a65034fdf61aae62cdecdb7d89c770"}, ] [package.extras] @@ -646,13 +664,15 @@ test = ["tap.py"] [[package]] name = "decorator" -version = "5.1.1" +version = "5.2.1" description = "Decorators for Humans" optional = true -python-versions = ">=3.5" +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, + {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, + {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, ] [[package]] @@ -661,6 +681,8 @@ version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, @@ -672,6 +694,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "test"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -686,6 +710,7 @@ version = "1.2.0" description = "Infer file type and MIME type of any file/buffer. No external dependencies." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"}, {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, @@ -697,6 +722,7 @@ version = "3.1.0" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.9" +groups = ["main", "test", "typing"] files = [ {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, @@ -716,43 +742,48 @@ dotenv = ["python-dotenv"] [[package]] name = "flask-cors" -version = "5.0.0" -description = "A Flask extension adding a decorator for CORS support" +version = "5.0.1" +description = "A Flask extension simplifying CORS support" optional = true -python-versions = "*" +python-versions = "<4.0,>=3.9" +groups = ["main"] +markers = "extra == \"aura\" or extra == \"web\"" files = [ - {file = "Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc"}, - {file = "flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef"}, + {file = "flask_cors-5.0.1-py3-none-any.whl", hash = "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c"}, + {file = "flask_cors-5.0.1.tar.gz", hash = "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c"}, ] [package.dependencies] -Flask = ">=0.9" +flask = ">=0.9" +Werkzeug = ">=0.7" [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main", "test"] files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] [package.dependencies] certifi = "*" -h11 = ">=0.13,<0.15" +h11 = ">=0.16" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] @@ -762,13 +793,14 @@ trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.28.0" +version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ - {file = "httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc"}, - {file = "httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -778,7 +810,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -790,6 +822,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "test"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -804,6 +837,8 @@ version = "0.2.0" description = "Cross-platform network interface and IP address enumeration library" optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"sonosupdate\"" files = [ {file = "ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748"}, {file = "ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4"}, @@ -815,6 +850,8 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -822,103 +859,95 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.7.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test", "typing"] +markers = "python_version == \"3.9\"" files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] name = "inflate64" -version = "1.0.0" +version = "1.0.1" description = "deflate64 compression/decompression library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "inflate64-1.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a90c0bdf4a7ecddd8a64cc977181810036e35807f56b0bcacee9abb0fcfd18dc"}, - {file = "inflate64-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57fe7c14aebf1c5a74fc3b70d355be1280a011521a76aa3895486e62454f4242"}, - {file = "inflate64-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d90730165f471d61a1a694a5e354f3ffa938227e8dcecb62d5d728e8069cee94"}, - {file = "inflate64-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:543f400201f5c101141af3c79c82059e1aa6ef4f1584a7f1fa035fb2e465097f"}, - {file = "inflate64-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ceca14f7ec19fb44b047f56c50efb7521b389d222bba2b0a10286a0caeb03fa"}, - {file = "inflate64-1.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b559937a42f0c175b4d2dfc7eb53b97bdc87efa9add15ed5549c6abc1e89d02f"}, - {file = "inflate64-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ff8bd2a562343fcbc4eea26fdc368904a3b5f6bb8262344274d3d74a1de15bb"}, - {file = "inflate64-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0fe481f31695d35a433c3044ac8fd5d9f5069aaad03a0c04b570eb258ce655aa"}, - {file = "inflate64-1.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a45f6979ad5874d4d4898c2fc770b136e61b96b850118fdaec5a5af1b9123a"}, - {file = "inflate64-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:022ca1cc928e7365a05f7371ff06af143c6c667144965e2cf9a9236a2ae1c291"}, - {file = "inflate64-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46792ecf3565d64fd2c519b0a780c03a57e195613c9954ef94e739a057b3fd06"}, - {file = "inflate64-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a70ea2e456c15f7aa7c74b8ab8f20b4f8940ec657604c9f0a9de3342f280fff"}, - {file = "inflate64-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e243ea9bd36a035059f2365bd6d156ff59717fbafb0255cb0c75bf151bf6904"}, - {file = "inflate64-1.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4dc392dec1cd11cacda3d2637214ca45e38202e8a4f31d4a4e566d6e90625fc4"}, - {file = "inflate64-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8b402a50eda7ee75f342fc346d33a41bca58edc222a4b17f9be0db1daed459fa"}, - {file = "inflate64-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:f5924499dc8800928c0ee4580fa8eb4ffa880b2cce4431537d0390e503a9c9ee"}, - {file = "inflate64-1.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0c644bf7208e20825ca3bbb5fb1f7f495cfcb49eb01a5f67338796d44a42f2bf"}, - {file = "inflate64-1.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9964a4eaf26a9d36f82a1d9b12c28e35800dd3d99eb340453ed12ac90c2976a8"}, - {file = "inflate64-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2cccded63865640d03253897be7232b2bbac295fe43914c61f86a57aa23bb61d"}, - {file = "inflate64-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d491f104fb3701926ebd82b8c9250dfba0ddcab584504e26f1e4adb26730378d"}, - {file = "inflate64-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ebad4a6cd2a2c1d81be0b09d4006479f3b258803c49a9224ef8ca0b649072fa"}, - {file = "inflate64-1.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6823b2c0cff3a8159140f3b17ec64fb8ec0e663b45a6593618ecdde8aeecb5b2"}, - {file = "inflate64-1.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:228d504239d27958e71fc77e3119a6ac4528127df38468a0c95a5bd3927204b8"}, - {file = "inflate64-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae2572e06bcfe15e3bbf77d4e4a6d6c55e2a70d6abceaaf60c5c3653ddb96dfd"}, - {file = "inflate64-1.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c10ca61212a753bbce6d341e7cfa779c161b839281f1f9fdc15cf1f324ce7c5b"}, - {file = "inflate64-1.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a982dc93920f9450da4d4f25c5e5c1288ef053b1d618cedc91adb67e035e35f5"}, - {file = "inflate64-1.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ca0310b2c55bc40394c5371db2a22f705fd594226cc09432e1eb04d3aed83930"}, - {file = "inflate64-1.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e95044ae55a161144445527a2efad550851fecc699066423d24b2634a6a83710"}, - {file = "inflate64-1.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34de6902c39d9225459583d5034182d371fc694bc3cfd6c0fc89aa62e9809faf"}, - {file = "inflate64-1.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ebafbd813213dc470719cd0a2bcb53aab89d9059f4e75386048b4c4dcdb2fd99"}, - {file = "inflate64-1.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75448c7b414dadaeeb11dab9f75e022aa1e0ee19b00f570e9f58e933603d71ac"}, - {file = "inflate64-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:2be4e01c1b04761874cb44b35b6103ca5846bc36c18fc3ff5e8cbcd8bfc15e9f"}, - {file = "inflate64-1.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bf2981b95c1f26242bb084d9a07f3feb0cfe3d6d0a8d90f42389803bc1252c4a"}, - {file = "inflate64-1.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9373ccf0661cc72ac84a0ad622634144da5ce7d57c9572ed0723d67a149feed2"}, - {file = "inflate64-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4650c6f65011ec57cf5cd96b92d5b7c6f59e502930c86eb8227c93cf02dc270"}, - {file = "inflate64-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a475e8822f1a74c873e60b8f270773757ade024097ca39e43402d47c049c67d4"}, - {file = "inflate64-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4367480733ac8daf368f6fc704b7c9db85521ee745eb5bd443f4b97d2051acc"}, - {file = "inflate64-1.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c5775c91f94f5eced9160fb0af12a09f3e030194f91a6a46e706a79350bd056"}, - {file = "inflate64-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d76d205b844d78ce04768060084ef20e64dcc63a3e9166674f857acaf4d140ed"}, - {file = "inflate64-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f0dc6af0e8e97324981178dc442956cbff1247a56d1e201af8d865244653f8"}, - {file = "inflate64-1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f79542478e49e471e8b23556700e6f688a40dc93e9a746f77a546c13251b59b1"}, - {file = "inflate64-1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a270be6b10cde01258c0097a663a307c62d12c78eb8f62f8e29f205335942c9"}, - {file = "inflate64-1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1616a87ff04f583e9558cc247ec0b72a30d540ee0c17cc77823be175c0ec92f0"}, - {file = "inflate64-1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:137ca6b315f0157a786c3a755a09395ca69aed8bcf42ad3437cb349f5ebc86d2"}, - {file = "inflate64-1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8140942d1614bdeb5a9ddd7559348c5c77f884a42424aef7ccf149ccfb93aa08"}, - {file = "inflate64-1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fe3f9051338bb7d07b5e7d88420d666b5109f33ae39aa55ecd1a053c0f22b1b"}, - {file = "inflate64-1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36342338e957c790fc630d4afcdcc3926beb2ecaea0b302336079e8fa37e57a0"}, - {file = "inflate64-1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:9b65cc701ef33ab20dbfd1d64088ffd89a8c265b356d2c21ba0ec565661645ef"}, - {file = "inflate64-1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dd6d3e7d47df43210a995fd1f5989602b64de3f2a17cf4cbff553518b3577fd4"}, - {file = "inflate64-1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f033b2879696b855200cde5ca4e293132c7499df790acb2c0dacb336d5e83b1"}, - {file = "inflate64-1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f816d1c8a0593375c289e285c96deaee9c2d8742cb0edbd26ee05588a9ae657"}, - {file = "inflate64-1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1facd35319b6a391ee4c3d709c7c650bcada8cd7141d86cd8c2257287f45e6e6"}, - {file = "inflate64-1.0.0.tar.gz", hash = "sha256:3278827b803cf006a1df251f3e13374c7d26db779e5a33329cc11789b804bc2d"}, + {file = "inflate64-1.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5122a188995e47a735ab969edc9129d42bbd97b993df5a3f0819b87205ce81b4"}, + {file = "inflate64-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:975ed694c680e46a5c0bb872380a9c9da271a91f9c0646561c58e8f3714347d4"}, + {file = "inflate64-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bcaf445d9cda5f7358e0c2b78144641560f8ce9e3e4351099754c49d26a34e8"}, + {file = "inflate64-1.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daede09baba24117279109b30fdf935195e91957e31b995b86f8dd01711376ee"}, + {file = "inflate64-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df40eaaba4fb8379d5c4fa5f56cc24741c4f1a91d4aef66438207473351ceaa"}, + {file = "inflate64-1.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ef90855ff63d53c8fd3bfbf85b5280b22f82b9ab2e21a7eee45b8a19d9866c42"}, + {file = "inflate64-1.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5daa4566c0b009c9ab8a6bf18ce407d14f5dbbb0d3068f3a43af939a17e117a7"}, + {file = "inflate64-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:d58a360b59685561a8feacee743479a9d7cc17c8d210aa1f2ae221f2513973cb"}, + {file = "inflate64-1.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31198c5f156806cee05b69b149074042b7b7d39274ff4c259b898e617294ac17"}, + {file = "inflate64-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ab693bb1cd92573a997f8fe7b90a2ec1e17a507884598f5640656257b95ef49"}, + {file = "inflate64-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:95b6a60e305e6e759e37d6c36691fcb87678922c56b3ddc2df06cd56e04f41f6"}, + {file = "inflate64-1.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:711ef889bdb3b3b296881d1e49830a3a896938fba7033c4287f1aed9b9a20111"}, + {file = "inflate64-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3178495970ecb5c6a32167a8b57fdeef3bf4e2843eaf8f2d8f816f523741e36"}, + {file = "inflate64-1.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e8373b7feedf10236eb56d21598a19a3eb51077c3702d0ce3456b827374025e1"}, + {file = "inflate64-1.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf026d5c885f2d2bbf233e9a0c8c6d046ec727e2467024ffe0ac76b5be308258"}, + {file = "inflate64-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:3aa7489241e6c6f6d34b9561efdf06031c35305b864267a5b8f406abcd3e85c5"}, + {file = "inflate64-1.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b81b3d373190ecd82901f42afd90b7127e9bdef341032a94db381c750ed3ddb2"}, + {file = "inflate64-1.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbfddc5dac975227c20997f0ac515917a15421767c6bff0c209ac6ff9d7b17cc"}, + {file = "inflate64-1.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2adeabe79cc2f90bca832673520c8cbad7370f86353e151293add7ca529bed34"}, + {file = "inflate64-1.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b235c97a05dbe2f92f0f057426e4d05a449e1fccf8e9aa88075ea9c6a06a182"}, + {file = "inflate64-1.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19b74e30734dca5f1c83ca07074e1f25bf7b63f4a5ee7e074d9a4cb05af65cd5"}, + {file = "inflate64-1.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b298feb85204b5ef148ccf807744c836fffed7c1ed3ec8bc9b4e323a03163291"}, + {file = "inflate64-1.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8a4c75241bc442267f79b8242135f2ded29405662c44b9353d34fbd4fa6e56b3"}, + {file = "inflate64-1.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:7b210392f0830ab27371e36478592f47757f5ea6c09ddb96e2125847b309eb5e"}, + {file = "inflate64-1.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8dd58aa1adc4f98bf9b52baffa8f2ddf589e071a90db2f2bec9024328d4608cf"}, + {file = "inflate64-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c108be2b87e88c966570f84f839eb37f489b45dc3fa3046dc228327af6e921bb"}, + {file = "inflate64-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63971c6b096c0d533c0e38b4257f5a7748501a8bc04d00cf239bd06467888703"}, + {file = "inflate64-1.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d0077edb6b1cabfa2223b71a4a725e5755148f551a7a396c7d5698e45fb8828"}, + {file = "inflate64-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f05b5f2a6f1bf2f70e9c20d997261711cbc1ae477379662b05b36911da60a67"}, + {file = "inflate64-1.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c7402165f7e15789caa0787e5a349465d9a454105d0c3a0ccf2e9cdfb8117"}, + {file = "inflate64-1.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39bced168822e4bf2f545d1b6dbeded6db01c32629d9e4549ef2cd1604a12e1b"}, + {file = "inflate64-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:70bb6a22d300d8ca25c26bc60afb5662c5a96d97a801962874d0461568512789"}, + {file = "inflate64-1.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f3d5ea758358a1cc50f9e8e41de2134e9b5c5ca8bbcd88d1cd135d0e953d0fa8"}, + {file = "inflate64-1.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fa102c834314c3d7edbf249d1be0bce5d12a9e122228a7ac3f861ee82c3dc5c"}, + {file = "inflate64-1.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c2ae56a34e6cc2a712418ac82332e5d550ef8599e0ffb64c19b86d63a7df0c5"}, + {file = "inflate64-1.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9808ae50b5db661770992566e51e648cac286c32bd80892b151e7b1eca81afe8"}, + {file = "inflate64-1.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:04b2788c6a26e1e525f53cc3d8c58782d41f18bef8d2a34a3d58beaaf0bfdd3b"}, + {file = "inflate64-1.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67fd5b1f9e433b0abab8cb91f4da94d16223a5241008268a57f4729fdbfc4dbc"}, + {file = "inflate64-1.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6f3b00c17ae365e82fc3d48ff9a7a566820a6c8c55b4e16c6cfbcbd46505a72"}, + {file = "inflate64-1.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:91c0c1d41c1655fb0189630baaa894a3b778d77062bb90ca11db878422948395"}, + {file = "inflate64-1.0.1.tar.gz", hash = "sha256:3b1c83c22651b5942b35829df526e89602e494192bf021e0d7d0b600e76c429d"}, ] [package.extras] -check = ["check-manifest", "flake8", "flake8-black", "flake8-deprecated", "isort (>=5.0.3)", "mypy (>=0.940)", "mypy-extensions (>=0.4.1)", "pygments", "readme-renderer", "twine"] +check = ["check-manifest", "flake8", "flake8-black", "flake8-deprecated", "flake8-isort", "mypy (>=1.10.0)", "mypy_extensions (>=0.4.1)", "pygments", "readme-renderer", "twine"] docs = ["docutils", "sphinx (>=5.0)"] -test = ["pyannotate", "pytest"] +test = ["pytest"] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -927,6 +956,7 @@ version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = false python-versions = ">=3.8" +groups = ["main", "test", "typing"] files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -934,103 +964,99 @@ files = [ [[package]] name = "jellyfish" -version = "1.1.0" +version = "1.2.0" description = "Approximate and phonetic matching of strings." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "jellyfish-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:feb1fa5838f2bb6dbc9f6d07dabf4b9d91e130b289d72bd70dc33b651667688f"}, - {file = "jellyfish-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:623fa58cca9b8e594a46e7b9cf3af629588a202439d97580a153d6af24736a1b"}, - {file = "jellyfish-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a87e4a17006f7cdd7027a053aeeaacfb0b3366955e242cd5b74bbf882bafe022"}, - {file = "jellyfish-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f10fa36491840bda29f2164cc49e61244ea27c5db5a66aaa437724f5626f5610"}, - {file = "jellyfish-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24f91daaa515284cdb691b1e01b0f91f9c9e51e685420725a1ded4f54d5376ff"}, - {file = "jellyfish-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:65e58350618ebb1488246998a7356a8c9a7c839ec3ecfe936df55be6776fc173"}, - {file = "jellyfish-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5c5ed62b23093b11de130c3fe1b381a2d3bfaf086757fa21341ac6f30a353e92"}, - {file = "jellyfish-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c42aa02e791d3e5a8fc6a96bec9f64ebbb2afef27b01eca201b56132e3d0c64e"}, - {file = "jellyfish-1.1.0-cp310-none-win32.whl", hash = "sha256:84680353261161c627cbdd622ea4243e3d3da75894bfacc2f3fcbbe56e8e59d4"}, - {file = "jellyfish-1.1.0-cp310-none-win_amd64.whl", hash = "sha256:017c794b89d827d0306cb056fc5fbd040ff558a90ff0e68a6b60d6e6ba661fe3"}, - {file = "jellyfish-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fed2e4ecf9b4995d2aa771453d0a0fdf47a5e1b13dbd74b98a30cb0070ede30c"}, - {file = "jellyfish-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61a382ba8a3d3cd0bd50029062d54d3a0726679be248789fef6a3901eee47a60"}, - {file = "jellyfish-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a4b526ed2080b97431454075c46c19baddc944e95cc605248e32a2a07be231e"}, - {file = "jellyfish-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0fa7450c3217724b73099cb18ee594926fcbc1cc4d9964350f31a4c1dc267b35"}, - {file = "jellyfish-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ebb6e9647d5d52f4d461a163449f6d1c73f1a80ccbe98bb17efac0062a6423"}, - {file = "jellyfish-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:759172602343115f910d7c63b39239051e32425115bc31ab4dafdaf6177f880c"}, - {file = "jellyfish-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:273fdc362ccdb09259eec9bc4abdc2467d9a54bd94d05ae22e71423dd1357255"}, - {file = "jellyfish-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bd5c335f8d762447691dc0572f4eaf0cfdfbfffb6dce740341425ab1b32134ff"}, - {file = "jellyfish-1.1.0-cp311-none-win32.whl", hash = "sha256:cc16a60a42f1541ad9c13c72c797107388227f01189aa3c0ec7ee9b939e57ea8"}, - {file = "jellyfish-1.1.0-cp311-none-win_amd64.whl", hash = "sha256:95dfe61eabf360a92e6d76d1c4dbafa29bcb3f70e2ad7354de2661141fcce038"}, - {file = "jellyfish-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:828a7000d369cbd4d812b88510c01fdab20b73dc54c63cdbe03bdff67ab362d0"}, - {file = "jellyfish-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e250dc1074d730a03c96ac9dfce44716cf45e0e2825cbddaf32a015cdf9cf594"}, - {file = "jellyfish-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87dc2a82c45b773a579fb695a5956a54106c1187f27c9ccee8508726d2e59cfc"}, - {file = "jellyfish-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41677ec860454da5977c698fc64fed73b4054a92c5c62ba7d1af535f8082ac7"}, - {file = "jellyfish-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d4002d01252f18eb26f28b66f6c9ce0696221804d8769553c5912b2f221a18"}, - {file = "jellyfish-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:936df26c10ca6cd6b4f0fb97753087354c568e2129c197cbb4e0f0172db7511f"}, - {file = "jellyfish-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:684c2093fa0d68a91146e15a1e9ca859259b19d3bc36ec4d60948d86751f744e"}, - {file = "jellyfish-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fcaefebe9d67f282d89d3a66646b77184a42b3eca2771636789b2dc1288c003"}, - {file = "jellyfish-1.1.0-cp312-none-win32.whl", hash = "sha256:e512c99941a257541ffd9f75c7a5c4689de0206841b72f1eb015599d17fed2c3"}, - {file = "jellyfish-1.1.0-cp312-none-win_amd64.whl", hash = "sha256:2b928bad2887c662783a4d9b5828ed1fa0e943f680589f7fc002c456fc02e184"}, - {file = "jellyfish-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d510b04e2a39f27aef391ca18bf527ec5d9a2438a63731b87faada83996cb92"}, - {file = "jellyfish-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57d005cc5daa4d0a8d88341d86b1dce24e3f1d7721da75326c0b7af598a4f58c"}, - {file = "jellyfish-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889edab0fb2a29d29c148c9327752df525c9bdaef03eef01d1bd9c1f90b47ebf"}, - {file = "jellyfish-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:937b657aacba8fe8482ebc5fea5ba1aee987ecb9da0f037bfb8a1a9045d05746"}, - {file = "jellyfish-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cb5088436ce1fdabcb46aed3a3cc215f0432313596f4e5abe5300ed833b697c"}, - {file = "jellyfish-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:af74156301a0ff05a22e8cf46250678e23fa447279ba6dffbf9feff01128f51d"}, - {file = "jellyfish-1.1.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3f978bc430bbed4df3c10b2a66be7b5bddd09e6c2856c7a17fa2298fb193d4d4"}, - {file = "jellyfish-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:b460f0bbde533f6f8624c1d7439e7f511b227ca18a58781e7f38f21961bd3f09"}, - {file = "jellyfish-1.1.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:7cd4b706cb6c4739846d78a398c67996cb451b09a732a625793cfe8d4f37af1b"}, - {file = "jellyfish-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61cded25b47fe6b4c2ea9478c0a5a7531845218525a1b2627c67907ee9fe9b15"}, - {file = "jellyfish-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04bf33577059afba33227977e4a2c08ccb954eb77c849fde564af3e31ee509d9"}, - {file = "jellyfish-1.1.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:327496501a44fbdfe0602fdc6a7d4317a7598202f1f652c9c4f0a49529a385cd"}, - {file = "jellyfish-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0d1e6bac549cc2919b83d0ebe26566404ae3dfef5ef86229d1d826e3aeaba4b"}, - {file = "jellyfish-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b5fec525f15b39687dbfd75589333df4e6f6d15d3b1e0ada02bf206363dfd2af"}, - {file = "jellyfish-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8b2faf015e86a9efd5679b3abde83cbd8f3104b9e89445aa76b8481b206b3e67"}, - {file = "jellyfish-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b73efda07d52a1583afb8915a5f9feb017d0b60ae6d03071b21cc4f0a8a08ec1"}, - {file = "jellyfish-1.1.0-cp38-none-win32.whl", hash = "sha256:4a5199583a956d313be825972d7c14a0d9e455884acd12c03d05e4272c6c3bb8"}, - {file = "jellyfish-1.1.0-cp38-none-win_amd64.whl", hash = "sha256:755b68920a839f9e2b4813f0990a8dadcc9a24980bb29839f636ab5e36aaa256"}, - {file = "jellyfish-1.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e965241e54f9cb9be6fe8f7a1376b6cc61ff831de017bde9150156771820f669"}, - {file = "jellyfish-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e59a4c3bf0847dfff44195a4c250bc9e281b1c403f6212534ee36fc7c913dc1"}, - {file = "jellyfish-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84fa4e72b7754060d352604e07ea89af98403b0436caad443276ae46135b7fd7"}, - {file = "jellyfish-1.1.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:125e9bfd1cc2c053eae3afa04fa142bbc8b3c1290a40a3416271b221f7e6bc87"}, - {file = "jellyfish-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4a8fff36462bf1bdaa339d58fadd7e79a63690902e6d7ddd65a84efc3a4cc6d"}, - {file = "jellyfish-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6b438b3d7f970cfd8f77b30b05694537a54c08f3775b35debae45ff5a469f1a5"}, - {file = "jellyfish-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:cf8d26c3735b5c2764cc53482dec14bb9b794ba829db3cd4c9a29d194a61cada"}, - {file = "jellyfish-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f341d0582ecac0aa73f380056dc8d25d8a60104f94debe8bf3f924a32a11588d"}, - {file = "jellyfish-1.1.0-cp39-none-win32.whl", hash = "sha256:49f2be59573b22d0adb615585ff66ca050198ec1f9f6784eec168bcd8137caf5"}, - {file = "jellyfish-1.1.0-cp39-none-win_amd64.whl", hash = "sha256:c58988138666b1cd860004c1afc7a09bb402e71e16e1f324be5c5d2b85fdfa3e"}, - {file = "jellyfish-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54effec80c7a5013bea8e2ea6cd87fdd35a2c5b35f86ccf69ec33f4212245f25"}, - {file = "jellyfish-1.1.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12ae67e9016c9a173453023fd7b400ec002bbc106c12722d914c53951acfa190"}, - {file = "jellyfish-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd342f9d4fb0ead8a3c30fe26e442308fb665ca37f4aa97baf448d814469bf1"}, - {file = "jellyfish-1.1.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b0dc9f1bb335b6caa412c3d27028e25d315ef2bc993d425db93e451d7bc28056"}, - {file = "jellyfish-1.1.0-pp310-pypy310_pp73-musllinux_1_1_i686.whl", hash = "sha256:3f12cb59b3266e37ec47bd7c2c37faadc74ae8ccdc0190444daeafda3bd93da2"}, - {file = "jellyfish-1.1.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c7ea99734b7767243b5b98eca953f0d719b48b0d630af3965638699728ef7523"}, - {file = "jellyfish-1.1.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1a90889fdb96ca27fc176e19a472c736e044d7190c924d9b7cfb0444881f921c"}, - {file = "jellyfish-1.1.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:c01cdf0d52d07e07fb0dfa2b3c03ca3b5a07088f08b38b06376ed228d842e501"}, - {file = "jellyfish-1.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a4678a2623cc83fde7ff683ba78d308edf7e54a1c81dd295cdf525761b9fcc1"}, - {file = "jellyfish-1.1.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b557b8e1fdad4a36f467ee44f5532a4a13e5300b93b2b5e70ff75d0d16458132"}, - {file = "jellyfish-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5c34d12730d912bafab9f6daaa7fb2c6fa6afc0a8fc2c4cdc017df485d8d843"}, - {file = "jellyfish-1.1.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d977a1e0fa3814d517b16d58a39a16e449bbd900b966dd921e770d0fd67bfa45"}, - {file = "jellyfish-1.1.0-pp38-pypy38_pp73-musllinux_1_1_i686.whl", hash = "sha256:6662152bf510cc7daef18965dd80cfa98710b479bda87a3170c86c4e0a6dc1ab"}, - {file = "jellyfish-1.1.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e447e3807c73aeda7b592919c105bf98ce0297a228aff68aafe4fe70a39b9a78"}, - {file = "jellyfish-1.1.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ca252e6088c6afe5f8138ce9f557157ad0329f0610914ba50729c641d57cd662"}, - {file = "jellyfish-1.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b2512ab6a1625a168796faaa159e1d1b8847cb3d0cc2b1b09ae77ff0623e7d10"}, - {file = "jellyfish-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b868da3186306efb48fbd8a8dee0a742a5c8bc9c4c74aa5003914a8600435ba8"}, - {file = "jellyfish-1.1.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bcc2cb1f007ddfad2f9175a8c1f934a8a0a6cc73187e2339fe1a4b3fd90b263e"}, - {file = "jellyfish-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e17885647f3a0faf1518cf6b319865b2e84439cfc16a3ea14468513c0fba227"}, - {file = "jellyfish-1.1.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:84ea543d05e6b7a7a704d45ebd9c753e2425da01fc5000ddc149031be541c4d5"}, - {file = "jellyfish-1.1.0-pp39-pypy39_pp73-musllinux_1_1_i686.whl", hash = "sha256:065a59ab0d02969d45e5ab4b0315ed6f5977a4eb8eaef24f2589e25b85822d18"}, - {file = "jellyfish-1.1.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f747f34071e1558151b342a2bf96b813e04b5384024ba7c50f3c907fbaab484f"}, - {file = "jellyfish-1.1.0.tar.gz", hash = "sha256:2a2eec494c81dc1eb23dfef543110dad1873538eccaffabea8520bdac8aecbc1"}, + {file = "jellyfish-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:50b6d2a123d3e0929cf136c6c26a6774338be7eb9d140a94223f56e3339b2f80"}, + {file = "jellyfish-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:baa1e44244cba9da6d67a50d3076dd7567e3b91caa9887bb68e20f321e0d2500"}, + {file = "jellyfish-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65082288f76b3821e5cfeae6c2290621f9e8eff75e3fe2d90817dcd068c5bf36"}, + {file = "jellyfish-1.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4309d536a5427c008bab096fd38cb2d44c7e475c494b23f2554cfdcf8a19f7fb"}, + {file = "jellyfish-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa0ac18693162b751bdd010a2c35136500a326b6a0bd0b18e6d973c524048ac7"}, + {file = "jellyfish-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c19d70cfbfe4eb9c7c6a1df848cfa48f6e5166a3f23362a2c1d7a2e763674113"}, + {file = "jellyfish-1.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5616698941afc730aa6cc162a4686bda29aa8127d70bcb939321143b1170238a"}, + {file = "jellyfish-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b28ad8da20561f07ca4bf1297bd5519d8cd947b8b5593e00cc7ceb255b2a1d84"}, + {file = "jellyfish-1.2.0-cp310-cp310-win32.whl", hash = "sha256:6ec6db8301bf91de85ee5084a44f0b5d399cc030c86910730d5ae69f1e712438"}, + {file = "jellyfish-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:8ed2393f6d0c7e8ec53ab0627dc81e1613bc2e76a9c30332197d0a1df5e39356"}, + {file = "jellyfish-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b4f8ff3cda0e00f6f62fe98ffce28bd7f21d1d55875470f8275a2fdbd84cfb6a"}, + {file = "jellyfish-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792cb481816626396892bccf53643ccc55a7f7c2b129de61360d01044a539afd"}, + {file = "jellyfish-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca2c84d3aaeea4bd7c9bdb174229789e69c7dd58916b47813f52db3a1b62495"}, + {file = "jellyfish-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ebc962fd90b2dcb33eb308e70c3a356a931c4b10c76d8d9d63df1d5dac42be4"}, + {file = "jellyfish-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0d765888bf186b75bf16b3d9a1b7f088f5f5ccbf62b414c25d92b404aad9c2a"}, + {file = "jellyfish-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:85c5eca0d56241d07a0a89f2896bc7d1ec66ee72ffa801847c70f404b0214fad"}, + {file = "jellyfish-1.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:13d7d925760bd8c3fd8831fcc0ad5a32ceae82c66e8aa19df45082afe5c4be2a"}, + {file = "jellyfish-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ccc330b6104c87e22dbb22c2578abcf0e36d1346c1810eec3f67571089b36874"}, + {file = "jellyfish-1.2.0-cp311-cp311-win32.whl", hash = "sha256:75d131a51202e679b653507f99634bc13c4aa6a4afabe06a1c3d200f72e18b9b"}, + {file = "jellyfish-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:63f58a0a7c9c0bb9a69562d2b9dd1a3f6729e94b0dcb6adf54b45b4da853eb94"}, + {file = "jellyfish-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:088c9b7e7077802ce2254b876486ae3b49d81f4f07f6c692c612ba40e1a42177"}, + {file = "jellyfish-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:393664438fbb98886f9c97675179d4b552b68c3d0099d4df3cdec6412deaeea0"}, + {file = "jellyfish-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a54a83905596dd712948b6af7fccc2b28d37624bfc9eab4868518c3f8106c739"}, + {file = "jellyfish-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2f68cdb689b59653fa70345c8fcb09bfee12d34c0f7ae223ce70fa5175cb2ee"}, + {file = "jellyfish-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:019542af342973c537275b289c1e891fb2b62b011bfdb68c816da4527477b74d"}, + {file = "jellyfish-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:079ec6fceb5336e7c2f99b43ee035f85b39022db897c70e736439ed1d4fc8462"}, + {file = "jellyfish-1.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a5ddd20e6d87c7dc173717ffe0df0bba50aa0b0c51e3d57d6cce1706ea6a1167"}, + {file = "jellyfish-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:08a1a3f6adc033eb3735a8ba121188a5d3fdc6630eec6a946c30260c1ac680ac"}, + {file = "jellyfish-1.2.0-cp312-cp312-win32.whl", hash = "sha256:65ec39cfed29e475df33c9d7fc70d76eb39ce6dfb7fedf19599caff497a9b3c7"}, + {file = "jellyfish-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:199baa59412723ef76126303fc236728b2613a4723fba83eede942c89e1dad1c"}, + {file = "jellyfish-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8b995bdf97d43cdca1e6bd5375f351bcb85c7f5e8760fe4a28c63eb0e6104075"}, + {file = "jellyfish-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:559c1d6f17ba51639843b958a0d57ece5c4155e6b820c4acb3f3437437625ef3"}, + {file = "jellyfish-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4439f4066ccc5dd6a7a15cb06941f5150bab646201e9e014a7d34d65cbe89fe"}, + {file = "jellyfish-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbf866d2b967fd2d5380134fdcb47d4f113e24d659b46c38e55da80c215d2042"}, + {file = "jellyfish-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9290b82276bba1941ad0f98226f51b43aeef7bdedb927b9266516b4519b9012"}, + {file = "jellyfish-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:052345ded2b00104a50acbab35c671efe06f40790202f6a2fc279ad645f31ab2"}, + {file = "jellyfish-1.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:465dcf8b17162e3dae2cae0072b22ea9637e6ce8ddd8294181758437cd9c0673"}, + {file = "jellyfish-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ae5f2e3c5ef14cb5b86afe7ed4078e504f75dd61ca9d9560bef597f9d2237c63"}, + {file = "jellyfish-1.2.0-cp313-cp313-win32.whl", hash = "sha256:13ee212b6fa294a1b6306693a1553b760d876780e757b9f016010748fe811b4d"}, + {file = "jellyfish-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c8089e918ddb1abae946e92d053f646a7f686d0d051ef69cdfaa28b37352bbdf"}, + {file = "jellyfish-1.2.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:50a0c79a663ccb919ba0b36af726aeefb72538481aca45b4f0708e104d2ef8af"}, + {file = "jellyfish-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c84fac793e43567c0c0361a6ad1bf5bc7126c2d130d5bdb5e0dffec72e805605"}, + {file = "jellyfish-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c68a293baa2748a484345c34bd97edaa83cd4a52d09204b1eecfbce177f3db01"}, + {file = "jellyfish-1.2.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:52a612f91bf979b188c46535218ac633ef62c9dab24b92324f181f985c9260a8"}, + {file = "jellyfish-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:057849127aa217601186eaec36a3ca5ed0bc86424c88bedc6c3ae2bf3c0c7616"}, + {file = "jellyfish-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4c8d6c28fb631c3d501b3c9ce1f7a729827b8dcd98e0f8599748f446a8ab94db"}, + {file = "jellyfish-1.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d2f87d8621c0c3122fa4307b247a258dbffd83cc30575e38497b869dcc711cf6"}, + {file = "jellyfish-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ccb573b94a60792144946a9dcd47f7d8b7b605a100c3d4b359d2d0aa17ee554"}, + {file = "jellyfish-1.2.0-cp39-cp39-win32.whl", hash = "sha256:83b0c09808be387e10172cb3dfc7229fc395a92db02bf7fbcd01e5546340a7ae"}, + {file = "jellyfish-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:59dc22ebda3a55701f422222b6e1afa75a83d2195accb7f42f7a796a1725fc1b"}, + {file = "jellyfish-1.2.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0d9d4136341c34ed0fb3fb406801d7a2724a1fa4996577822bc5e5b27870b4"}, + {file = "jellyfish-1.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36e53729ac0bd387c8f7fd60c8727e6784113976058d8aa2f62398fcdfe209f1"}, + {file = "jellyfish-1.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10b178b8351e90e463d937e53c8b5525627a0bb2ca6f7e49ac7452b0608a1020"}, + {file = "jellyfish-1.2.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e79225fba34bb082d2f21fa5cd7dc015c856ff04b8340e98ce122ab71a445cad"}, + {file = "jellyfish-1.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18a1ea2d395a4c9c00224b3abd57b73bd2cb4ca17fc6e2024c8433b31e1b1061"}, + {file = "jellyfish-1.2.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b27ff486175b9c8fecdc9147c7dde1d835675bd76df12c1521f378a2fab70493"}, + {file = "jellyfish-1.2.0-pp310-pypy310_pp73-musllinux_1_1_i686.whl", hash = "sha256:4b6b1109174e6ae780f6c2b8454b2fcfc7efeb8fae767f757c481ccca16fb842"}, + {file = "jellyfish-1.2.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfb4e76c2b6c3e94ea3778510c94609893ddc7172255838b3221eba1ec9aa5cc"}, + {file = "jellyfish-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:07384e33e5f9bfd3d1356cf73d94388af295ed8f196a1d9f09bc381c5ea79be8"}, + {file = "jellyfish-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:03754194fc2f5cf65136f2b5f2aeacf48a805ddf21f4ff9f1a6cffc67756d937"}, + {file = "jellyfish-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57a0c408c588c4477bdcd82c0c1c33f08900aca5c2dfc9d5e78f2e0919294a68"}, + {file = "jellyfish-1.2.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72062c9772ff672535463954828e9921fb1bf1d63c66602db2956567e9e50aa8"}, + {file = "jellyfish-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb3b464faeb8e4f4f6f7987fbd3f5de759fc0d460bbe4768b446e3f1c003026a"}, + {file = "jellyfish-1.2.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33c5d80209b278807a770a463f39d0b6a3f95dacf9a64fd322ad4add63a52516"}, + {file = "jellyfish-1.2.0-pp311-pypy311_pp73-musllinux_1_1_i686.whl", hash = "sha256:0787a5fef60aa838732f325064cc4401425e450023bb8fc8d3b2bd2ee75df57d"}, + {file = "jellyfish-1.2.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dee4cc60f2b342f3f62784787f1ba811e505b9a8d8f68cc7505d496c563143b5"}, + {file = "jellyfish-1.2.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b726088637c1fef88a9cbcd91ba9960dfd58d9604506083c4902092db13f71c"}, + {file = "jellyfish-1.2.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c3f9413fadf4bc9d453822c4e13ccf15a5d831857b387cfbb85ba153afbe974a"}, + {file = "jellyfish-1.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04162e0803029146eb102b2fb11e1be6a966fdd48ea5bd22399b75e5edfbd73e"}, + {file = "jellyfish-1.2.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d5dbbbd69091969b605aaf317cda8e11413732934c471f9ba82aceedb9d83333"}, + {file = "jellyfish-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03ae5049f8ba478e1acb1a2cbaa4062d45c2a76d32d66d52260f979c0c0701c7"}, + {file = "jellyfish-1.2.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:07d9b9708c2b91059ba635cfdb04665cf7ca9055a70c9a2b590da4755395aba0"}, + {file = "jellyfish-1.2.0-pp39-pypy39_pp73-musllinux_1_1_i686.whl", hash = "sha256:cc2190b04a7759c1f5d2095aa711d99ff803b22d6c863b6e60b4fb531d2aed7b"}, + {file = "jellyfish-1.2.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:8c984b6f7ccc682f5e6211b2e722a8cac2ccca0dae312df83fa70a341302f6ee"}, + {file = "jellyfish-1.2.0.tar.gz", hash = "sha256:5c7d73db4045dcc53b6efbfea21f3d3da432d3e052dc51827574d1a447fc23b4"}, ] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main", "test", "typing"] files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -1041,13 +1067,15 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "joblib" -version = "1.4.2" +version = "1.5.0" description = "Lightweight pipelining with Python functions" optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ - {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, - {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, + {file = "joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491"}, + {file = "joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5"}, ] [[package]] @@ -1056,6 +1084,8 @@ version = "1.0.9" description = "Language detection library ported from Google's language-detection." optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"fetchart\" or extra == \"lyrics\"" files = [ {file = "langdetect-1.0.9-py2-none-any.whl", hash = "sha256:7cbc0746252f19e76f77c0b1690aadf01963be835ef0cd4b56dddf2a8f1dfc2a"}, {file = "langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0"}, @@ -1070,6 +1100,7 @@ version = "0.5.12" description = "Linear Assignment Problem solver (LAPJV/LAPMOD)." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "lap-0.5.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c3a38070b24531949e30d7ebc83ca533fcbef6b1d6562f035cae3b44dfbd5ec"}, {file = "lap-0.5.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a301dc9b8a30e41e4121635a0e3d0f6374a08bb9509f618d900e18d209b815c4"}, @@ -1136,6 +1167,8 @@ version = "0.4" description = "Makes it easy to load subpackages and functions on demand." optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ {file = "lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc"}, {file = "lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1"}, @@ -1155,6 +1188,8 @@ version = "0.10.2.post1" description = "Python module for audio and music processing" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ {file = "librosa-0.10.2.post1-py3-none-any.whl", hash = "sha256:dc882750e8b577a63039f25661b7e39ec4cfbacc99c1cffba666cd664fb0a7a0"}, {file = "librosa-0.10.2.post1.tar.gz", hash = "sha256:cd99f16717cbcd1e0983e37308d1db46a6f7dfc2e396e5a9e61e6821e44bd2e7"}, @@ -1186,6 +1221,8 @@ version = "0.43.0" description = "lightweight wrapper around basic LLVM functionality" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ {file = "llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761"}, {file = "llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc"}, @@ -1212,157 +1249,150 @@ files = [ [[package]] name = "lxml" -version = "5.3.0" +version = "5.4.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"sonosupdate\"" files = [ - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, - {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, - {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, - {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, - {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, - {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, - {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, - {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, - {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, - {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, - {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, - {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, - {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, - {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, - {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, - {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, - {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, - {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, - {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, - {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, - {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, - {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, + {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c"}, + {file = "lxml-5.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5"}, + {file = "lxml-5.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776"}, + {file = "lxml-5.4.0-cp310-cp310-win32.whl", hash = "sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7"}, + {file = "lxml-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9"}, + {file = "lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8"}, + {file = "lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd"}, + {file = "lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751"}, + {file = "lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4"}, + {file = "lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4"}, + {file = "lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7"}, + {file = "lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f"}, + {file = "lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc"}, + {file = "lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f"}, + {file = "lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0"}, + {file = "lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8"}, + {file = "lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b"}, + {file = "lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a"}, + {file = "lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82"}, + {file = "lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f"}, + {file = "lxml-5.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf"}, + {file = "lxml-5.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410"}, + {file = "lxml-5.4.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c"}, + {file = "lxml-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56"}, + {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, + {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, + {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, + {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, + {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, + {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, + {file = "lxml-5.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa"}, + {file = "lxml-5.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84"}, + {file = "lxml-5.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6"}, + {file = "lxml-5.4.0-cp38-cp38-win32.whl", hash = "sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88"}, + {file = "lxml-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e"}, + {file = "lxml-5.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433"}, + {file = "lxml-5.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5"}, + {file = "lxml-5.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142"}, + {file = "lxml-5.4.0-cp39-cp39-win32.whl", hash = "sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6"}, + {file = "lxml-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571"}, + {file = "lxml-5.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1"}, + {file = "lxml-5.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325"}, + {file = "lxml-5.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d"}, + {file = "lxml-5.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987"}, + {file = "lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml-html-clean"] +html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.11)"] +source = ["Cython (>=3.0.11,<3.1.0)"] [[package]] name = "markupsafe" @@ -1370,6 +1400,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main", "test", "typing"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1440,6 +1471,7 @@ version = "0.13.0" description = "Handles low-level interfacing for files' tags. Wraps Mutagen to" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mediafile-0.13.0-py3-none-any.whl", hash = "sha256:cd8d183d0e0671b5203a86e92cf4e3338ecc892a1ec9dcd7ec0ed87779e514cb"}, {file = "mediafile-0.13.0.tar.gz", hash = "sha256:de71063e1bffe9733d6ccad526ea7dac8a9ce760105827f81ab0cb034c729a6d"}, @@ -1454,13 +1486,14 @@ test = ["tox"] [[package]] name = "mock" -version = "5.1.0" +version = "5.2.0" description = "Rolling backport of unittest.mock for all Pythons" optional = false python-versions = ">=3.6" +groups = ["test"] files = [ - {file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"}, - {file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"}, + {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}, + {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}, ] [package.extras] @@ -1474,6 +1507,8 @@ version = "1.1.0" description = "MessagePack serializer" optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, @@ -1547,6 +1582,7 @@ version = "0.2.3" description = "multi volume file wrapper library" optional = false python-versions = ">=3.6" +groups = ["main", "test"] files = [ {file = "multivolumefile-0.2.3-py3-none-any.whl", hash = "sha256:237f4353b60af1703087cf7725755a1f6fcaeeea48421e1896940cd1c920d678"}, {file = "multivolumefile-0.2.3.tar.gz", hash = "sha256:a0648d0aafbc96e59198d5c17e9acad7eb531abea51035d08ce8060dcad709d6"}, @@ -1563,6 +1599,7 @@ version = "0.7.1" description = "Python bindings for the MusicBrainz NGS and the Cover Art Archive webservices" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] files = [ {file = "musicbrainzngs-0.7.1-py2.py3-none-any.whl", hash = "sha256:e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10"}, {file = "musicbrainzngs-0.7.1.tar.gz", hash = "sha256:ab1c0100fd0b305852e65f2ed4113c6de12e68afd55186987b8ed97e0f98e627"}, @@ -1574,6 +1611,7 @@ version = "1.47.0" description = "read and write audio tags for many formats" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"}, {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, @@ -1581,49 +1619,50 @@ files = [ [[package]] name = "mypy" -version = "1.13.0" +version = "1.15.0" description = "Optional static typing for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["typing"] files = [ - {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, - {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, - {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, - {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, - {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, - {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, - {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, - {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, - {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, - {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, - {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, - {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, - {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, - {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, - {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, - {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, - {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, - {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, - {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, - {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, - {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, - {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, + {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, + {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, + {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, + {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, + {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, + {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, + {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, + {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, + {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, + {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, + {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, + {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, ] [package.dependencies] -mypy-extensions = ">=1.0.0" +mypy_extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -1634,13 +1673,14 @@ reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "1.0.0" +version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" +groups = ["typing"] files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] [[package]] @@ -1649,6 +1689,8 @@ version = "0.60.0" description = "compiling Python code using LLVM" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ {file = "numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651"}, {file = "numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b"}, @@ -1683,6 +1725,7 @@ version = "2.0.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, @@ -1737,6 +1780,7 @@ version = "3.2.2" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false python-versions = ">=3.6" +groups = ["main", "test"] files = [ {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, @@ -1749,122 +1793,133 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "packaging" -version = "24.2" +version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "release", "test"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] [[package]] name = "pillow" -version = "11.0.0" +version = "11.2.1" description = "Python Imaging Library (Fork)" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"aura\" or extra == \"embedart\" or extra == \"fetchart\" or extra == \"thumbnails\"" files = [ - {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, - {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, - {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, - {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, - {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, - {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, - {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, - {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, - {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, - {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, - {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, - {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, - {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, - {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, - {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, - {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, - {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, - {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, - {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, - {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, - {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, - {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, - {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, - {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, - {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, - {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, - {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, - {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, - {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, - {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, - {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, - {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, + {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"}, + {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"}, + {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"}, + {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"}, + {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"}, + {file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"}, + {file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"}, + {file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"}, + {file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"}, + {file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"}, + {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"}, + {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"}, + {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"}, + {file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"}, + {file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"}, + {file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"}, + {file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"}, + {file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"}, + {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"}, + {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"}, + {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"}, + {file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"}, + {file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"}, + {file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"}, + {file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"}, + {file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"}, + {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"}, + {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"}, + {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"}, + {file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"}, + {file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"}, + {file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"}, + {file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"}, + {file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"}, + {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"}, + {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"}, + {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"}, + {file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"}, + {file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"}, + {file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"}, + {file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"}, + {file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"}, + {file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"}, + {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"}, + {file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"}, + {file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"}, + {file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"}, + {file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"}, + {file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"}, + {file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"}, + {file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] +test-arrow = ["pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" @@ -1872,6 +1927,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1887,6 +1943,8 @@ version = "1.8.2" description = "A friend to fetch your data files" optional = true python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ {file = "pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47"}, {file = "pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10"}, @@ -1904,32 +1962,27 @@ xxhash = ["xxhash (>=1.4.3)"] [[package]] name = "psutil" -version = "6.1.0" -description = "Cross-platform lib for process and system monitoring in Python." +version = "7.0.0" +description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.6" +groups = ["main", "test"] +markers = "sys_platform != \"cygwin\"" files = [ - {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, - {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, - {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, - {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, - {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, - {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, - {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, - {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, + {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, + {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, + {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, + {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, + {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, ] [package.extras] -dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] test = ["pytest", "pytest-xdist", "setuptools"] [[package]] @@ -1938,6 +1991,7 @@ version = "0.22.0" description = "Pure python 7-zip library" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "py7zr-0.22.0-py3-none-any.whl", hash = "sha256:993b951b313500697d71113da2681386589b7b74f12e48ba13cc12beca79d078"}, {file = "py7zr-0.22.0.tar.gz", hash = "sha256:c6c7aea5913535184003b73938490f9a4d8418598e533f9ca991d3b8e45a139e"}, @@ -1968,6 +2022,8 @@ version = "1.3.0" description = "bindings for Chromaprint acoustic fingerprinting and the Acoustid API" optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"chroma\"" files = [ {file = "pyacoustid-1.3.0.tar.gz", hash = "sha256:5f4f487191c19ebb908270b1b7b5297f132da332b1568b96a914574c079ed177"}, ] @@ -1978,76 +2034,89 @@ requests = "*" [[package]] name = "pybcj" -version = "1.0.2" +version = "1.0.6" description = "bcj filter library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "pybcj-1.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7bff28d97e47047d69a4ac6bf59adda738cf1d00adde8819117fdb65d966bdbc"}, - {file = "pybcj-1.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:198e0b4768b4025eb3309273d7e81dc53834b9a50092be6e0d9b3983cfd35c35"}, - {file = "pybcj-1.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa26415b4a118ea790de9d38f244312f2510a9bb5c65e560184d241a6f391a2d"}, - {file = "pybcj-1.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fabb2be57e4ca28ea36c13146cdf97d73abd27c51741923fc6ba1e8cd33e255c"}, - {file = "pybcj-1.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d6d613bae6f27678d5e44e89d61018779726aa6aa950c516d33a04b8af8c59"}, - {file = "pybcj-1.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ffae79ef8a1ea81ea2748ad7b7ad9b882aa88ddf65ce90f9e944df639eccc61"}, - {file = "pybcj-1.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bdb4d8ff5cba3e0bd1adee7d20dbb2b4d80cb31ac04d6ea1cd06cfc02d2ecd0d"}, - {file = "pybcj-1.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a29be917fbc99eca204b08407e0971e0205bfdad4b74ec915930675f352b669d"}, - {file = "pybcj-1.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2562ebe5a0abec4da0229f8abb5e90ee97b178f19762eb925c1159be36828b3"}, - {file = "pybcj-1.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:af19bc61ded933001cd68f004ae2042bf1a78eb498a3c685ebd655fa1be90dbe"}, - {file = "pybcj-1.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3f4a447800850aba7724a2274ea0a4800724520c1caf38f7d0dabf2f89a5e15"}, - {file = "pybcj-1.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1c8af7a4761d2b1b531864d84113948daa0c4245775c63bd9874cb955f4662"}, - {file = "pybcj-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8007371f6f2b462f5aa05d5c2135d0a1bcf5b7bdd9bd15d86c730f588d10b7d3"}, - {file = "pybcj-1.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1079ca63ff8da5c936b76863690e0bd2489e8d4e0a3a340e032095dae805dd91"}, - {file = "pybcj-1.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e9a785eb26884429d9b9f6326e68c3638828c83bf6d42d2463c97ad5385caff2"}, - {file = "pybcj-1.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:9ea46e2d45469d13b7f25b08efcdb140220bab1ac5a850db0954591715b8caaa"}, - {file = "pybcj-1.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:21b5f2460629167340403d359289a173e0729ce8e84e3ce99462009d5d5e01a4"}, - {file = "pybcj-1.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2940fb85730b9869254559c491cd83cf777e56c76a8a60df60e4be4f2a4248d7"}, - {file = "pybcj-1.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f40f3243139d675f43793a4e35c410c370f7b91ccae74e70c8b2f4877869f90e"}, - {file = "pybcj-1.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c2b3e60b65c7ac73e44335934e1e122da8d56db87840984601b3c5dc0ae4c19"}, - {file = "pybcj-1.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746550dc7b5af4d04bb5fa4d065f18d39c925bcb5dee30db75747cd9a58bb6e8"}, - {file = "pybcj-1.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8ce9b62b6aaa5b08773be8a919ecc4e865396c969f982b685eeca6e80c82abb7"}, - {file = "pybcj-1.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:493eab2b1f6f546730a6de0c5ceb75ce16f3767154e8ae30e2b70d41b928b7d2"}, - {file = "pybcj-1.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ef55b96b7f2ed823e0b924de902065ec42ade856366c287dbb073fabd6b90ec1"}, - {file = "pybcj-1.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ed5b3dd9c209fe7b90990dee4ef21870dca39db1cd326553c314ee1b321c1cc"}, - {file = "pybcj-1.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22a94885723f8362d4cb468e68910eef92d3e2b1293de82b8eacb4198ef6655f"}, - {file = "pybcj-1.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b8f9368036c9e658d8e3b3534086d298a5349c864542b34657cbe57c260daa49"}, - {file = "pybcj-1.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87108181c7a6ac4d3fc1e4551cab5db5eea7f9fdca611175243234cd94bcc59b"}, - {file = "pybcj-1.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db57f26b8c0162cfddb52b869efb1741b8c5e67fc536994f743074985f714c55"}, - {file = "pybcj-1.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bdf5bcac4f1da36ad43567ea6f6ef404347658dbbe417c87cdb1699f327d6337"}, - {file = "pybcj-1.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c3171bb95c9b45cbcad25589e1ae4f4ca4ea99dc1724c4e0671eb6b9055514e"}, - {file = "pybcj-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:f9a2585e0da9cf343ea27421995b881736a1eb604a7c1d4ca74126af94c3d4a8"}, - {file = "pybcj-1.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fdb7cd8271471a5979d84915c1ee57eea7e0a69c893225fc418db66883b0e2a7"}, - {file = "pybcj-1.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e96ae14062bdcddc3197300e6ee4efa6fbc6749be917db934eac66d0daaecb68"}, - {file = "pybcj-1.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a54ebdc8423ba99d75372708a882fcfc3b14d9d52cf195295ad53e5a47dab37f"}, - {file = "pybcj-1.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3602be737c6e9553c45ae89e6b0e556f64f34dabf27d5260317d1824d31b79d3"}, - {file = "pybcj-1.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63dd2ca52a48841f561bfec0fa3f208d375b0a8dcd3d7b236459e683ae29221d"}, - {file = "pybcj-1.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8204a714029784b1a08a3d790430d80b423b68615c5b1e67aabca5bd5419b77d"}, - {file = "pybcj-1.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fde2376b180ae2620c102fbc3ef06638d306feae83964aaa5051ecbdda54845a"}, - {file = "pybcj-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:3b8d7810fb587adbffba025330cf212d9bbed8f29559656d05cb6609673f306a"}, - {file = "pybcj-1.0.2.tar.gz", hash = "sha256:c7f5bef7f47723c53420e377bc64d2553843bee8bcac5f0ad076ab1524780018"}, + {file = "pybcj-1.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0fc8eda59e9e52d807f411de6db30aadd7603aa0cb0a830f6f45226b74be1926"}, + {file = "pybcj-1.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0495443e8691510129f0c589ed956af4962c22b7963c5730b0c80c9c5b818c06"}, + {file = "pybcj-1.0.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c7998b546c3856dbe9ae879cb90393df80507f65097e7019785852769f4a990"}, + {file = "pybcj-1.0.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c859f85e718924f48b3ac967cda5528ccbef1e448a4462652cca688eee477"}, + {file = "pybcj-1.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:186fbb849883ac80764d96dbd253503dd9cecbcf6133504a0c9d6a2df81d5746"}, + {file = "pybcj-1.0.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:437bd5f5e6579bde404404ad2de915d1306c389595c68d0eb8933fee1408e951"}, + {file = "pybcj-1.0.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:933d6be8f07c653ff3eba16900376b3212249be1c71caf9db17f4cd52da5076c"}, + {file = "pybcj-1.0.6-cp310-cp310-win_amd64.whl", hash = "sha256:90e169b669bbed30e22d36ba97d23dcfc71e044d3be41c8010fd6a53950725e5"}, + {file = "pybcj-1.0.6-cp310-cp310-win_arm64.whl", hash = "sha256:06441026c773f8abeb7816566acfffe7cd65a9b69094197a9de64d0496cd4c3c"}, + {file = "pybcj-1.0.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0275564a1afc4b2d1a6ff465384fb73a64622a88b6e4856cb7964ba2335a06e"}, + {file = "pybcj-1.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa794b134b4ee183a4ceb739e9c3a445a24ee12e7e3231c37820f66848db4c52"}, + {file = "pybcj-1.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d8945e8157c7fa469db110fc78579d154a31d121d14705b26d7d3ec3a471c8e"}, + {file = "pybcj-1.0.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7109177b4f77526a6ce4b565ee37483f5a5dd29bc92eaea6739b3c58618aeb7"}, + {file = "pybcj-1.0.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c48cbc9ebed137ac8759d0f2c3d12b999581dae7b4f84d974888c402f00fdb78"}, + {file = "pybcj-1.0.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6dccff82008e3cb5e5e639737320c02341b8718e189b9ece13f0230e0d57e7af"}, + {file = "pybcj-1.0.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e4e68cfc4fb099e8200386ac2255a9f514b8bb056189273bcce874bda3597459"}, + {file = "pybcj-1.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:13747c01b60bf955878267718f28c36e2bbb81fb8495b0173b21083c7d08a4a4"}, + {file = "pybcj-1.0.6-cp311-cp311-win_arm64.whl", hash = "sha256:6f81d6106c50c5e91c16ad58584fd7ab9eb941360188547e0184b1ede9e47f1d"}, + {file = "pybcj-1.0.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f5d1dbc76f615595d7d8f3846c07f607fb1e2305d085c34556b32dacf8e88d12"}, + {file = "pybcj-1.0.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1398f556ed2afe16ae363a2b6e8cf6aeda3aa21861757286bc6c498278886c60"}, + {file = "pybcj-1.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e269cfc7b6286af87c5447c9f8c685f19cff011cac64947ffb4cd98919696a7f"}, + {file = "pybcj-1.0.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7393d0b0dcaa0b1a7850245def78fa14438809e9a3f73b1057a975229d623fd3"}, + {file = "pybcj-1.0.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e252891698d3e01d0f60eb5adfe849038cd2d429cb9510f915a0759301f1884d"}, + {file = "pybcj-1.0.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ae5c891fcda9d5a6826a1b8e843b1e52811358594121553e6683e65b13eccce7"}, + {file = "pybcj-1.0.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:eac3cb317df1cefed2783ce9cafdae61899dd02f2f4749dc0f4494a7c425745f"}, + {file = "pybcj-1.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:72ebec5cda5a48de169c2d7548ea2ce7f48732de0175d7e0e665ca7360eaa4c4"}, + {file = "pybcj-1.0.6-cp312-cp312-win_arm64.whl", hash = "sha256:8f1f75a01e45d01ecf88d31910ca1ace5d345e3bfb7c18db0af3d0c393209b63"}, + {file = "pybcj-1.0.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3e6800eb599ce766e588095eedb2a2c45a93928d1880420e8ecfad7eff0c73dc"}, + {file = "pybcj-1.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69a841ca0d3df978a2145488cec58460fa4604395321178ba421384cff26062f"}, + {file = "pybcj-1.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:887521da03302c96048803490073bd0423ff408a3adca2543c6ee86bc0af7578"}, + {file = "pybcj-1.0.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39a5a9a2d0e1fa4ddd2617a549c11e5022888af86dc8e29537cfee7f5761127d"}, + {file = "pybcj-1.0.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57757bc382f326bd93eb277a9edfc8dff6c22f480da467f0c5a5d63b9d092a41"}, + {file = "pybcj-1.0.6-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb1872b24b30d8473df433f3364e828b021964229d47a07f7bfc08496dbfd23e"}, + {file = "pybcj-1.0.6-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:5fedfeed96ab0e34207097f663b94e8c7076025c2c7af6a482e670e808ea5bb0"}, + {file = "pybcj-1.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:caefc3109bf172ad37b52e21dc16c84cf495b2ea2890cc7256cdf0188914508d"}, + {file = "pybcj-1.0.6-cp313-cp313-win_arm64.whl", hash = "sha256:b24367175528da452a19e4c55368d5c907f4584072dc6aeee8990e2a5e6910fc"}, + {file = "pybcj-1.0.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:558128fbc201c9f11c1b1df30377fab3821ebb736c28e5eaf9fff9cc9e56b806"}, + {file = "pybcj-1.0.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d05f4026154d77c97486d5ce04261b473e3ec8c2f7cf0f937b7baa439c616559"}, + {file = "pybcj-1.0.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96ce9c428800ecc0d52cec9947ee167f3a7f913cc2ba58b9a462e7f19c52ac4b"}, + {file = "pybcj-1.0.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05038a58d78ab15a847ed90c17d924be5b7848f27a43517dc88a5589fba1ca78"}, + {file = "pybcj-1.0.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:591f58891ff52585a38894b28c8b952e4c7be93f65d6d43751672cde8edeff36"}, + {file = "pybcj-1.0.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1f416250101631ac04705a19d78ec407d261da9dffa0e1fa1f1f2d9409ec70d"}, + {file = "pybcj-1.0.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:27c489dd9e0d9745ebf7cd4344f23b6cb655edb2dea879ca63a0558a993e0d4b"}, + {file = "pybcj-1.0.6-cp39-cp39-win_amd64.whl", hash = "sha256:56fe3ff939653c6b0e35aa105170af3494ee9e2469494ef1d0fa2bac3fdd99d0"}, + {file = "pybcj-1.0.6-cp39-cp39-win_arm64.whl", hash = "sha256:6c88e1a04b90547f0470e4d2bd190bbe6b73c8666d4f7196c3ca43a379a15de5"}, + {file = "pybcj-1.0.6.tar.gz", hash = "sha256:70bbe2dc185993351955bfe8f61395038f96f5de92bb3a436acb01505781f8f2"}, ] [package.extras] -check = ["check-manifest", "flake8 (<5)", "flake8-black", "flake8-colors", "flake8-isort", "flake8-pyi", "flake8-typing-imports", "mypy (>=0.812)", "mypy-extensions (>=0.4.3)", "pygments", "readme-renderer"] +check = ["check-manifest", "flake8 (<5)", "flake8-black", "flake8-colors", "flake8-isort", "flake8-pyi", "flake8-typing-imports", "mypy (>=1.10.0)", "pygments", "readme-renderer"] test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "pycairo" -version = "1.27.0" +version = "1.28.0" description = "Python interface for cairo" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"bpd\" or extra == \"replaygain\"" files = [ - {file = "pycairo-1.27.0-cp310-cp310-win32.whl", hash = "sha256:e20f431244634cf244ab6b4c3a2e540e65746eed1324573cf291981c3e65fc05"}, - {file = "pycairo-1.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:03bf570e3919901572987bc69237b648fe0de242439980be3e606b396e3318c9"}, - {file = "pycairo-1.27.0-cp311-cp311-win32.whl", hash = "sha256:9a9b79f92a434dae65c34c830bb9abdbd92654195e73d52663cbe45af1ad14b2"}, - {file = "pycairo-1.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:d40a6d80b15dacb3672dc454df4bc4ab3988c6b3f36353b24a255dc59a1c8aea"}, - {file = "pycairo-1.27.0-cp312-cp312-win32.whl", hash = "sha256:e2239b9bb6c05edae5f3be97128e85147a155465e644f4d98ea0ceac7afc04ee"}, - {file = "pycairo-1.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:27cb4d3a80e3b9990af552818515a8e466e0317063a6e61585533f1a86f1b7d5"}, - {file = "pycairo-1.27.0-cp313-cp313-win32.whl", hash = "sha256:01505c138a313df2469f812405963532fc2511fb9bca9bdc8e0ab94c55d1ced8"}, - {file = "pycairo-1.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:b0349d744c068b6644ae23da6ada111c8a8a7e323b56cbce3707cba5bdb474cc"}, - {file = "pycairo-1.27.0-cp39-cp39-win32.whl", hash = "sha256:f9ca8430751f1fdcd3f072377560c9e15608b9a42d61375469db853566993c9b"}, - {file = "pycairo-1.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b1321652a6e27c4de3069709b1cae22aed2707fd8c5e889c04a95669228af2a"}, - {file = "pycairo-1.27.0.tar.gz", hash = "sha256:5cb21e7a00a2afcafea7f14390235be33497a2cce53a98a19389492a60628430"}, + {file = "pycairo-1.28.0-cp310-cp310-win32.whl", hash = "sha256:53e6dbc98456f789965dad49ef89ce2c62f9a10fc96c8d084e14da0ffb73d8a6"}, + {file = "pycairo-1.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:c8ab91a75025f984bc327ada335c787efb61c929ea0512063793cb36cee503d4"}, + {file = "pycairo-1.28.0-cp310-cp310-win_arm64.whl", hash = "sha256:e955328c1a5147bf71ee94e206413ce15e12630296a79788fcd246c80e5337b8"}, + {file = "pycairo-1.28.0-cp311-cp311-win32.whl", hash = "sha256:0fee15f5d72b13ba5fd065860312493dc1bca6ff2dce200ee9d704e11c94e60a"}, + {file = "pycairo-1.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:6339979bfec8b58a06476094a9a5c104bd5a99932ddaff16ca0d9203d2f4482c"}, + {file = "pycairo-1.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6ae15392e28ebfc0b35d8dc05d395d3b6be4bad9ad4caecf0fa12c8e7150225"}, + {file = "pycairo-1.28.0-cp312-cp312-win32.whl", hash = "sha256:c00cfbb7f30eb7ca1d48886712932e2d91e8835a8496f4e423878296ceba573e"}, + {file = "pycairo-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:d50d190f5033992b55050b9f337ee42a45c3568445d5e5d7987bab96c278d8a6"}, + {file = "pycairo-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:957e0340ee1c279d197d4f7cfa96f6d8b48e453eec711fca999748d752468ff4"}, + {file = "pycairo-1.28.0-cp313-cp313-win32.whl", hash = "sha256:d13352429d8a08a1cb3607767d23d2fb32e4c4f9faa642155383980ec1478c24"}, + {file = "pycairo-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:082aef6b3a9dcc328fa648d38ed6b0a31c863e903ead57dd184b2e5f86790140"}, + {file = "pycairo-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:026afd53b75291917a7412d9fe46dcfbaa0c028febd46ff1132d44a53ac2c8b6"}, + {file = "pycairo-1.28.0-cp39-cp39-win32.whl", hash = "sha256:3ed16d48b8a79cc584cb1cb0ad62dfb265f2dda6d6a19ef5aab181693e19c83c"}, + {file = "pycairo-1.28.0-cp39-cp39-win_amd64.whl", hash = "sha256:da0d1e6d4842eed4d52779222c6e43d254244a486ca9fdab14e30042fd5bdf28"}, + {file = "pycairo-1.28.0-cp39-cp39-win_arm64.whl", hash = "sha256:458877513eb2125513122e8aa9c938630e94bb0574f94f4fb5ab55eb23d6e9ac"}, + {file = "pycairo-1.28.0.tar.gz", hash = "sha256:26ec5c6126781eb167089a123919f87baa2740da2cca9098be8b3a6b91cc5fbc"}, ] [[package]] @@ -2056,61 +2125,63 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {main = "extra == \"autobpm\" or extra == \"reflink\" or platform_python_implementation == \"PyPy\"", test = "platform_python_implementation == \"PyPy\""} [[package]] name = "pycryptodomex" -version = "3.21.0" +version = "3.22.0" description = "Cryptographic library for Python" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "test"] files = [ - {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, - {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, - {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, - {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:41673e5cc39a8524557a0472077635d981172182c9fe39ce0b5f5c19381ffaff"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:276be1ed006e8fd01bba00d9bd9b60a0151e478033e86ea1cb37447bbc057edc"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:813e57da5ceb4b549bab96fa548781d9a63f49f1d68fdb148eeac846238056b7"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-win32.whl", hash = "sha256:d7beeacb5394765aa8dabed135389a11ee322d3ee16160d178adc7f8ee3e1f65"}, + {file = "pycryptodomex-3.22.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:b3746dedf74787da43e4a2f85bd78f5ec14d2469eb299ddce22518b3891f16ea"}, + {file = "pycryptodomex-3.22.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5ebc09b7d8964654aaf8a4f5ac325f2b0cc038af9bea12efff0cd4a5bb19aa42"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:aef4590263b9f2f6283469e998574d0bd45c14fb262241c27055b82727426157"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5ac608a6dce9418d4f300fab7ba2f7d499a96b462f2b9b5c90d8d994cd36dcad"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a24f681365ec9757ccd69b85868bbd7216ba451d0f86f6ea0eed75eeb6975db"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:259664c4803a1fa260d5afb322972813c5fe30ea8b43e54b03b7e3a27b30856b"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7127d9de3c7ce20339e06bcd4f16f1a1a77f1471bcf04e3b704306dde101b719"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee75067b35c93cc18b38af47b7c0664998d8815174cfc66dd00ea1e244eb27e6"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a8b0c5ba061ace4bcd03496d42702c3927003db805b8ec619ea6506080b381d"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bfe4fe3233ef3e58028a3ad8f28473653b78c6d56e088ea04fe7550c63d4d16b"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-win32.whl", hash = "sha256:2cac9ed5c343bb3d0075db6e797e6112514764d08d667c74cb89b931aac9dddd"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:ff46212fda7ee86ec2f4a64016c994e8ad80f11ef748131753adb67e9b722ebd"}, + {file = "pycryptodomex-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:5bf3ce9211d2a9877b00b8e524593e2209e370a287b3d5e61a8c45f5198487e2"}, + {file = "pycryptodomex-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:684cb57812cd243217c3d1e01a720c5844b30f0b7b64bb1a49679f7e1e8a54ac"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c8cffb03f5dee1026e3f892f7cffd79926a538c67c34f8b07c90c0bd5c834e27"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:140b27caa68a36d0501b05eb247bd33afa5f854c1ee04140e38af63c750d4e39"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:644834b1836bb8e1d304afaf794d5ae98a1d637bd6e140c9be7dd192b5374811"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c506aba3318505dbeecf821ed7b9a9f86f422ed085e2d79c4fba0ae669920a"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7cd39f7a110c1ab97ce9ee3459b8bc615920344dc00e56d1b709628965fba3f2"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e4eaaf6163ff13788c1f8f615ad60cdc69efac6d3bf7b310b21e8cfe5f46c801"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eac39e237d65981554c2d4c6668192dc7051ad61ab5fc383ed0ba049e4007ca2"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ab0d89d1761959b608952c7b347b0e76a32d1a5bb278afbaa10a7f3eaef9a0a"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e64164f816f5e43fd69f8ed98eb28f98157faf68208cd19c44ed9d8e72d33e8"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f005de31efad6f9acefc417296c641f13b720be7dbfec90edeaca601c0fab048"}, + {file = "pycryptodomex-3.22.0.tar.gz", hash = "sha256:a1da61bacc22f93a91cbe690e3eb2022a03ab4123690ab16c46abb693a9df63d"}, ] [[package]] name = "pydata-sphinx-theme" -version = "0.16.0" +version = "0.16.1" description = "Bootstrap-based Sphinx theme from the PyData community" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ - {file = "pydata_sphinx_theme-0.16.0-py3-none-any.whl", hash = "sha256:18c810ee4e67e05281e371e156c1fb5bb0fa1f2747240461b225272f7d8d57d8"}, - {file = "pydata_sphinx_theme-0.16.0.tar.gz", hash = "sha256:721dd26e05fa8b992d66ef545536e6cbe0110afb9865820a08894af1ad6f7707"}, + {file = "pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde"}, + {file = "pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7"}, ] [package.dependencies] @@ -2131,13 +2202,15 @@ test = ["pytest", "pytest-cov", "pytest-regressions", "sphinx[test]"] [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = true python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"docs\"" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -2145,12 +2218,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pygobject" -version = "3.50.0" +version = "3.52.3" description = "Python bindings for GObject Introspection" optional = true python-versions = "<4.0,>=3.9" +groups = ["main"] +markers = "extra == \"bpd\" or extra == \"replaygain\"" files = [ - {file = "pygobject-3.50.0.tar.gz", hash = "sha256:4500ad3dbf331773d8dedf7212544c999a76fc96b63a91b3dcac1e5925a1d103"}, + {file = "pygobject-3.52.3.tar.gz", hash = "sha256:00e427d291e957462a8fad659a9f9c8be776ff82a8b76bdf402f1eaeec086d82"}, ] [package.dependencies] @@ -2158,13 +2233,14 @@ pycairo = ">=1.16" [[package]] name = "pylast" -version = "5.3.0" +version = "5.5.0" description = "A Python interface to Last.fm and Libre.fm" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "pylast-5.3.0-py3-none-any.whl", hash = "sha256:4cc47cdcb05baf24a5cea10a012c17df0fe13e22911296a69835b127458a7308"}, - {file = "pylast-5.3.0.tar.gz", hash = "sha256:637943b1b0e6045dd85ed7389db6071a1fea45cc7ff90dc6126fd509ca6fae2f"}, + {file = "pylast-5.5.0-py3-none-any.whl", hash = "sha256:a28b5dbf69ef71b868e42ce27c74e4feea5277fbee26960549604ce34d631bbe"}, + {file = "pylast-5.5.0.tar.gz", hash = "sha256:b6e95cf11fb99779cd451afd5dd68c4036c44f88733cf2346ba27317c1869da4"}, ] [package.dependencies] @@ -2175,99 +2251,85 @@ tests = ["flaky", "pytest", "pytest-cov", "pytest-random-order", "pyyaml"] [[package]] name = "pyppmd" -version = "1.1.0" +version = "1.1.1" description = "PPMd compression/decompression library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test"] files = [ - {file = "pyppmd-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5cd428715413fe55abf79dc9fc54924ba7e518053e1fc0cbdf80d0d99cf1442"}, - {file = "pyppmd-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e96cc43f44b7658be2ea764e7fa99c94cb89164dbb7cdf209178effc2168319"}, - {file = "pyppmd-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd20142869094bceef5ab0b160f4fff790ad1f612313a1e3393a51fc3ba5d57e"}, - {file = "pyppmd-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4f9b51e45c11e805e74ea6f6355e98a6423b5bbd92f45aceee24761bdc3d3b8"}, - {file = "pyppmd-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:459f85e928fb968d0e34fb6191fd8c4e710012d7d884fa2b317b2e11faac7c59"}, - {file = "pyppmd-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f73cf2aaf60477eef17f5497d14b6099d8be9748390ad2b83d1c88214d050c05"}, - {file = "pyppmd-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2ea3ae0e92c0b5345cd3a4e145e01bbd79c2d95355481ea5d833b5c0cb202a2d"}, - {file = "pyppmd-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:775172c740133c0162a01c1a5443d0e312246881cdd6834421b644d89a634b91"}, - {file = "pyppmd-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:14421030f1d46f69829698bdd960698a3b3df0925e3c470e82cfcdd4446b7bc1"}, - {file = "pyppmd-1.1.0-cp310-cp310-win32.whl", hash = "sha256:b691264f9962532aca3bba5be848b6370e596d0a2ca722c86df388be08d0568a"}, - {file = "pyppmd-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:216b0d969a3f06e35fbfef979706d987d105fcb1e37b0b1324f01ee143719c4a"}, - {file = "pyppmd-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1f8c51044ee4df1b004b10bf6b3c92f95ea86cfe1111210d303dca44a56e4282"}, - {file = "pyppmd-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac25b3a13d1ac9b8f0bde46952e10848adc79d932f2b548a6491ef8825ae0045"}, - {file = "pyppmd-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8d3003eebe6aabe22ba744a38a146ed58a25633420d5da882b049342b7c8036"}, - {file = "pyppmd-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c520656bc12100aa6388df27dd7ac738577f38bf43f4a4bea78e1861e579ea5"}, - {file = "pyppmd-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c2a3e807028159a705951f5cb5d005f94caed11d0984e59cc50506de543e22d"}, - {file = "pyppmd-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8a2447e69444703e2b273247bfcd4b540ec601780eff07da16344c62d2993d"}, - {file = "pyppmd-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b9e0c8053e69cad6a92a0889b3324f567afc75475b4f54727de553ac4fc85780"}, - {file = "pyppmd-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5938d256e8d2a2853dc3af8bb58ae6b4a775c46fc891dbe1826a0b3ceb624031"}, - {file = "pyppmd-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1ce5822d8bea920856232ccfb3c26b56b28b6846ea1b0eb3d5cb9592a026649e"}, - {file = "pyppmd-1.1.0-cp311-cp311-win32.whl", hash = "sha256:2a9e894750f2a52b03e3bc0d7cf004d96c3475a59b1af7e797d808d7d29c9ffe"}, - {file = "pyppmd-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:969555c72e72fe2b4dd944127521a8f2211caddb5df452bbc2506b5adfac539e"}, - {file = "pyppmd-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d6ef8fd818884e914bc209f7961c9400a4da50d178bba25efcef89f09ec9169"}, - {file = "pyppmd-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95f28e2ecf3a9656bd7e766aaa1162b6872b575627f18715f8b046e8617c124a"}, - {file = "pyppmd-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f3557ea65ee417abcdf5f49d35df00bb9f6f252639cae57aeefcd0dd596133"}, - {file = "pyppmd-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e84b25d088d7727d50218f57f92127cdb839acd6ec3de670b6680a4cf0b2d2a"}, - {file = "pyppmd-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99ed42891986dac8c2ecf52bddfb777900233d867aa18849dbba6f3335600466"}, - {file = "pyppmd-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6fe69b82634488ada75ba07efb90cd5866fa3d64a2c12932b6e8ae207a14e5f"}, - {file = "pyppmd-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:60981ffde1fe6ade750b690b35318c41a1160a8505597fda2c39a74409671217"}, - {file = "pyppmd-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46e8240315476f57aac23d71e6de003e122b65feba7c68f4cc46a089a82a7cd4"}, - {file = "pyppmd-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0308e2e76ecb4c878a18c2d7a7c61dbca89b4ef138f65d5f5ead139154dcdea"}, - {file = "pyppmd-1.1.0-cp312-cp312-win32.whl", hash = "sha256:b4fa4c27dc1314d019d921f2aa19e17f99250557e7569eeb70e180558f46af74"}, - {file = "pyppmd-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c269d21e15f4175df27cf00296476097af76941f948734c642d7fb6e85b9b3b9"}, - {file = "pyppmd-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a04ef5fd59818b035855723af85ce008c8191d31216706ffcbeedc505efca269"}, - {file = "pyppmd-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e3ebcf5f95142268afa5cc46457d9dab2d29a3ccfd020a1129dd9d6bd021be1"}, - {file = "pyppmd-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4ad046a9525d1f52e93bc642a4cec0bf344a3ba1a15923e424e7a50f8ca003d8"}, - {file = "pyppmd-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169e5023c86ed1f7587961900f58aa78ad8a3d59de1e488a2228b5ba3de52402"}, - {file = "pyppmd-1.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baf798e76edd9da975cc536f943756a1b1755eb8ed87371f86f76d7c16e8d034"}, - {file = "pyppmd-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d63be8c068879194c1e7548d0c57f54a4d305ba204cd0c7499b678f0aee893ef"}, - {file = "pyppmd-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5fc178a3c21af78858acbac9782fca6a927267694c452e0882c55fec6e78319"}, - {file = "pyppmd-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:28a1ab1ef0a31adce9b4c837b7b9acb01ce8f1f702ff3ff884f03d21c2f6b9bb"}, - {file = "pyppmd-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5fef43bfe98ada0a608adf03b2d205e071259027ab50523954c42eef7adcef67"}, - {file = "pyppmd-1.1.0-cp38-cp38-win32.whl", hash = "sha256:6b980902797eab821299a1c9f42fa78eff2826a6b0b0f6bde8a621f9765ffd55"}, - {file = "pyppmd-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:80cde69013f357483abe0c3ff30c55dc5e6b4f72b068f91792ce282c51dc0bff"}, - {file = "pyppmd-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aeea1bf585c6b8771fa43a6abd704da92f8a46a6d0020953af15d7f3c82e48c"}, - {file = "pyppmd-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7759bdb137694d4ab0cfa5ff2c75c212d90714c7da93544694f68001a0c38e12"}, - {file = "pyppmd-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db64a4fe956a2e700a737a1d019f526e6ccece217c163b28b354a43464cc495b"}, - {file = "pyppmd-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f788ae8f5a9e79cd777b7969d3401b2a2b87f47abe306c2a03baca30595e9bd"}, - {file = "pyppmd-1.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:324a178935c140210fca2043c688b77e79281da8172d2379a06e094f41735851"}, - {file = "pyppmd-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:363030bbcb7902fb9eeb59ffc262581ca5dd7790ba950328242fd2491c54d99b"}, - {file = "pyppmd-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:31b882584f86440b0ff7906385c9f9d9853e5799197abaafdae2245f87d03f01"}, - {file = "pyppmd-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b991b4501492ec3380b605fe30bee0b61480d305e98519d81c2a658b2de01593"}, - {file = "pyppmd-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6108044d943b826f97a9e79201242f61392d6c1fadba463b2069c4e6bc961e1"}, - {file = "pyppmd-1.1.0-cp39-cp39-win32.whl", hash = "sha256:c45ce2968b7762d2cacf622b0a8f260295c6444e0883fd21a21017e3eaef16ed"}, - {file = "pyppmd-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5289f32ab4ec5f96a95da51309abd1769f928b0bff62047b3bc25c878c16ccb"}, - {file = "pyppmd-1.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ad5da9f7592158e6b6b51d7cd15e536d8b23afbb4d22cba4e5744c7e0a3548b1"}, - {file = "pyppmd-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc6543e7d12ef0a1466d291d655e3d6bca59c7336dbb53b62ccdd407822fb52b"}, - {file = "pyppmd-1.1.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5e4008a45910e3c8c227f6f240de67eb14454c015dc3d8060fc41e230f395d3"}, - {file = "pyppmd-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9301fa39d1fb0ed09a10b4c5d7f0074113e96a1ead16ba7310bedf95f7ef660c"}, - {file = "pyppmd-1.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:59521a3c6028da0cb5780ba16880047b00163432a6b975da2f6123adfc1b0be8"}, - {file = "pyppmd-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d7ec02f1778dd68547e497625d66d7858ce10ea199146eb1d80ee23ba42954be"}, - {file = "pyppmd-1.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f062ca743f9b99fe88d417b4d351af9b4ff1a7cbd3d765c058bb97de976d57f1"}, - {file = "pyppmd-1.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088e326b180a0469ac936849f5e1e5320118c22c9d9e673e9c8551153b839c84"}, - {file = "pyppmd-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:897fa9ab5ff588a1000b8682835c5acf219329aa2bbfec478100e57d1204eeab"}, - {file = "pyppmd-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3af4338cc48cd59ee213af61d936419774a0f8600b9aa2013cd1917b108424f0"}, - {file = "pyppmd-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cce8cd2d4ceebe2dbf41db6dfebe4c2e621314b3af8a2df2cba5eb5fa277f122"}, - {file = "pyppmd-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62e57927dbcb91fb6290a41cd83743b91b9d85858efb16a0dd34fac208ee1c6b"}, - {file = "pyppmd-1.1.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:435317949a6f35e54cdf08e0af6916ace427351e7664ac1593980114668f0aaa"}, - {file = "pyppmd-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f66b0d0e32b8fb8707f1d2552f13edfc2917e8ed0bdf4d62e2ce190d2c70834"}, - {file = "pyppmd-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:650a663a591e06fb8096c213f4070b158981c8c3bf9c166ce7e4c360873f2750"}, - {file = "pyppmd-1.1.0.tar.gz", hash = "sha256:1d38ce2e4b7eb84b53bc8a52380b94f66ba6c39328b8800b30c2b5bf31693973"}, + {file = "pyppmd-1.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:406b184132c69e3f60ea9621b69eaa0c5494e83f82c307b3acce7b86a4f8f888"}, + {file = "pyppmd-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2cf003bb184adf306e1ac1828107307927737dde63474715ba16462e266cbef"}, + {file = "pyppmd-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71c8fd0ecc8d4760e852dd6df19d1a827427cb9e6c9e568cbf5edba7d860c514"}, + {file = "pyppmd-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6b5edee08b66ad6c39fd4d34a7ef4cfeb4b69fd6d68957e59cd2db674611a9e"}, + {file = "pyppmd-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e95bd23eb1543ab3149f24fe02f6dd2695023326027a4b989fb2c6dba256e75e"}, + {file = "pyppmd-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e633ee4cc19d0c71b3898092c3c4cc20a10bd5e6197229fffac29d68ad5d83b8"}, + {file = "pyppmd-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecaafe2807ef557f0c49b8476a4fa04091b43866072fbcf31b3ceb01a96c9168"}, + {file = "pyppmd-1.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c182fccff60ae8f24f28f5145c36a60708b5b041a25d36b67f23c44923552fa4"}, + {file = "pyppmd-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:70c93d19efe67cdac3e7fa2d4e171650a2c4f90127a9781b25e496a43f12fbbc"}, + {file = "pyppmd-1.1.1-cp310-cp310-win32.whl", hash = "sha256:57c75856920a210ed72b553885af7bc06eddfd30ff26b62a3a63cb8f86f3d217"}, + {file = "pyppmd-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d5293f10dc8c1d571b780e0d54426d3d858c19bbd8cb0fe972dcea3906acd05c"}, + {file = "pyppmd-1.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:753c5297c91c059443caef33bccbffb10764221739d218046981638aeb9bc5f2"}, + {file = "pyppmd-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b5a73da09de480a94793c9064876af14a01be117de872737935ac447b7cde3c"}, + {file = "pyppmd-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89c6febb7114dea02a061143d78d04751a945dfcadff77560e9a3d3c7583c24b"}, + {file = "pyppmd-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0001e467c35e35e6076a8c32ed9074aa45833615ee16115de9282d5c0985a1d8"}, + {file = "pyppmd-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c76820db25596afc859336ba06c01c9be0ff326480beec9c699fd378a546a77f"}, + {file = "pyppmd-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b67f0a228f8c58750a21ba667c170ae957283e08fd580857f13cb686334e5b3e"}, + {file = "pyppmd-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b18f24c14f0b0f1757a42c458ae7b6fd7aa0bce8147ac1016a9c134068c1ccc2"}, + {file = "pyppmd-1.1.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c9e43729161cc3b6ad5b04b16bae7665d3c0cc803de047d8a979aa9232a4f94a"}, + {file = "pyppmd-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fe057d254528b4eeebe2800baefde47d6af679bae184d3793c13a06f794df442"}, + {file = "pyppmd-1.1.1-cp311-cp311-win32.whl", hash = "sha256:faa51240493a5c53c9b544c99722f70303eea702742bf90f3c3064144342da4a"}, + {file = "pyppmd-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:62486f544d6957e1381147e3961eee647b7f4421795be4fb4f1e29d52aee6cb5"}, + {file = "pyppmd-1.1.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9877ef273e2c0efdec740855e28004a708ada9012e0db6673df4bb6eba3b05e0"}, + {file = "pyppmd-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f816a5cbccceced80e15335389eeeaf1b56a605fb7eebe135b1c85bd161e288c"}, + {file = "pyppmd-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6bddabf8f2c6b991d15d6785e603d9d414ae4a791f131b1a729bb8a5d31133d1"}, + {file = "pyppmd-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:855bc2b0d19c3fead5815d72dbe350b4f765334336cbf8bcb504d46edc9e9dd2"}, + {file = "pyppmd-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a95b11b3717c083b912f0879678ba72f301bbdb9b69efed46dbc5df682aa3ce7"}, + {file = "pyppmd-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38b645347b6ea217b0c58e8edac27473802868f152db520344ac8c7490981849"}, + {file = "pyppmd-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f8f94b6222262def5b532f2b9716554ef249ad8411fd4da303596cc8c2e8eda1"}, + {file = "pyppmd-1.1.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1c0306f69ceddf385ef689ebd0218325b7e523c48333d87157b37393466cfa1e"}, + {file = "pyppmd-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4ba510457a56535522a660098399e3fa8722e4de55808d089c9d13435d87069"}, + {file = "pyppmd-1.1.1-cp312-cp312-win32.whl", hash = "sha256:032f040a89fd8348109e8638f94311bd4c3c693fb4cad213ad06a37c203690b1"}, + {file = "pyppmd-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:2be8cbd13dd59fad1a0ad38062809e28596f3673b77a799dfe82b287986265ed"}, + {file = "pyppmd-1.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9458f972f090f3846fc5bea0a6f7363da773d3c4b2d4654f1d4ca3c11f6ecbfa"}, + {file = "pyppmd-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:44811a9d958873d857ca81cebf7ba646a0952f8a7bbf8a60cf6ec5d002faa040"}, + {file = "pyppmd-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a1b12460958885ca44e433986644009d0599b87a444f668ce3724a46ce588924"}, + {file = "pyppmd-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:200c74f05b97b00f047cf60607914a0b50f80991f1fb3677f624a85aa79d9458"}, + {file = "pyppmd-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ebe0d98a341b32f164e860059243e125398865cc0363b32ffc31f953460fe87"}, + {file = "pyppmd-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf93e1e047a82f1e7e194fcf49da166d2b9d8dc98d7c0b5cd844dc4360d9c1f5"}, + {file = "pyppmd-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f5b0b8c746bde378ae3b4df42a11fd8599ba3e5808dfea36e16d722b74bd0506"}, + {file = "pyppmd-1.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bcdd5207b6c79887f25639632ca2623a399d8c54f567973e9ba474b5ebae2b1c"}, + {file = "pyppmd-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7bfcca94e5452b6d54ac24a11c2402f6a193c331e5dc221c1f1df71773624374"}, + {file = "pyppmd-1.1.1-cp39-cp39-win32.whl", hash = "sha256:18e99c074664f996f511bc6e87aab46bc4c75f5bd0157d3210292919be35e22c"}, + {file = "pyppmd-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b29788d5a0f8f39ea46a1255cd886daddf9c64ba9d4cb64677bc93bd3859ac0e"}, + {file = "pyppmd-1.1.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28648ef56793bf1ed0ff24728642f56fa39cb96ea161dec6ee2d26f97c0cdd28"}, + {file = "pyppmd-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:427d6f9b9c011e032db9529b2a15773f2e2944ca490b67d5757f4af33bbda406"}, + {file = "pyppmd-1.1.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34c7a07197a03656c1920fd88e05049c155a955c4de4b8b8a8e5fec19a97b45b"}, + {file = "pyppmd-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1fea2eee28beca61165c4714dcd032de76af318553791107d308b4b08575ecc"}, + {file = "pyppmd-1.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:04391e4f82c8c2c316ba60e480300ad1af37ec12bdb5c20f06b502030ff35975"}, + {file = "pyppmd-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cf08a354864c352a94e6e53733009baeab1e7c570010c4f5be226923ecfa09d1"}, + {file = "pyppmd-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:334e5fe5d75764b87c591a16d2b2df6f9939e2ad114dacf98bb4b0e7c90911e9"}, + {file = "pyppmd-1.1.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d5928b25f04f5431585d17c835cd509a34e1c9f1416653db8d2815e97d4e20"}, + {file = "pyppmd-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af06329796a4965788910ac40f1b012d2e173ede08456ceea0ec7fc4d2e69d62"}, + {file = "pyppmd-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4ccdd3751e432e71e02de96f16fc8824e4f4bfc47a8b470f0c7aae88dae4c666"}, + {file = "pyppmd-1.1.1.tar.gz", hash = "sha256:f1a812f1e7628f4c26d05de340b91b72165d7b62778c27d322b82ce2e8ff00cb"}, ] [package.extras] -check = ["check-manifest", "flake8 (<5)", "flake8-black", "flake8-isort", "isort (>=5.0.3)", "mypy (>=0.812)", "mypy-extensions (>=0.4.3)", "pygments", "readme-renderer"] -docs = ["sphinx (>=2.3)", "sphinx-rtd-theme"] +check = ["check-manifest", "flake8", "flake8-black", "flake8-isort", "mypy (>=1.10.0)", "pygments", "readme-renderer"] +docs = ["sphinx", "sphinx_rtd_theme"] fuzzer = ["atheris", "hypothesis"] test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-benchmark", "pytest-cov", "pytest-timeout"] [[package]] name = "pytest" -version = "8.3.4" +version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] [package.dependencies] @@ -2283,13 +2345,14 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-cov" -version = "6.0.0" +version = "6.1.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["test"] files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, + {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, + {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, ] [package.dependencies] @@ -2305,6 +2368,7 @@ version = "1.3.0" description = "A set of py.test fixtures to test Flask applications." optional = false python-versions = ">=3.7" +groups = ["test"] files = [ {file = "pytest-flask-1.3.0.tar.gz", hash = "sha256:58be1c97b21ba3c4d47e0a7691eb41007748506c36bf51004f78df10691fa95e"}, {file = "pytest_flask-1.3.0-py3-none-any.whl", hash = "sha256:c0e36e6b0fddc3b91c4362661db83fa694d1feb91fa505475be6732b5bc8c253"}, @@ -2324,6 +2388,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "test"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2338,6 +2403,7 @@ version = "3.1.1" description = "A Python MPD client library" optional = false python-versions = ">=3.6" +groups = ["main", "test"] files = [ {file = "python-mpd2-3.1.1.tar.gz", hash = "sha256:4baec3584cc43ed9948d5559079fafc2679b06b2ade273e909b3582654b2b3f5"}, {file = "python_mpd2-3.1.1-py2.py3-none-any.whl", hash = "sha256:86bf1100a0b135959d74a9a7a58cf0515bf30bb54eb25ae6fb8e175e50300fc3"}, @@ -2348,13 +2414,14 @@ twisted = ["Twisted"] [[package]] name = "python3-discogs-client" -version = "2.7.1" +version = "2.8" description = "Python API client for Discogs" optional = false python-versions = "*" +groups = ["main", "test"] files = [ - {file = "python3_discogs_client-2.7.1-py3-none-any.whl", hash = "sha256:5fb5f3d2f288a8ce2c8c152444258bacedb35b7d61bc466bddae332b6c737444"}, - {file = "python3_discogs_client-2.7.1.tar.gz", hash = "sha256:f2453582f5d044ea5847d27cfe56473179e51c9a836913b46db803c20ae598f9"}, + {file = "python3_discogs_client-2.8-py3-none-any.whl", hash = "sha256:60d63a613da73afeb818015e680fa5f007ffaa94d97578070e7ee4f11dc1b1b3"}, + {file = "python3_discogs_client-2.8.tar.gz", hash = "sha256:0f2c77f4ff491a6ef60fe892032028df899808e65efcd48249b4ecf21146b33b"}, ] [package.dependencies] @@ -2371,6 +2438,7 @@ version = "0.28" description = "PyXDG contains implementations of freedesktop.org standards in python." optional = false python-versions = "*" +groups = ["main", "test"] files = [ {file = "pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"}, {file = "pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4"}, @@ -2382,6 +2450,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -2444,6 +2513,7 @@ version = "0.16.2" description = "Python bindings to Zstandard (zstd) compression library." optional = false python-versions = ">=3.5" +groups = ["main", "test"] files = [ {file = "pyzstd-0.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:637376c8f8cbd0afe1cab613f8c75fd502bd1016bf79d10760a2d5a00905fe62"}, {file = "pyzstd-0.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e7a7118cbcfa90ca2ddbf9890c7cb582052a9a8cf2b7e2c1bbaf544bee0f16a"}, @@ -2536,6 +2606,7 @@ version = "4.2" description = "RAR archive reader for Python" optional = false python-versions = ">=3.6" +groups = ["main", "test"] files = [ {file = "rarfile-4.2-py3-none-any.whl", hash = "sha256:8757e1e3757e32962e229cab2432efc1f15f210823cc96ccba0f6a39d17370c9"}, {file = "rarfile-4.2.tar.gz", hash = "sha256:8e1c8e72d0845ad2b32a47ab11a719bc2e41165ec101fd4d3fe9e92aa3f469ef"}, @@ -2547,6 +2618,8 @@ version = "0.2.2" description = "Python reflink wraps around platform specific reflink implementations" optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"reflink\"" files = [ {file = "reflink-0.2.2-cp36-cp36m-win32.whl", hash = "sha256:8435c7153af4d6e66dc8acb48a9372c8ec6f978a09cdf7b57cd6656d969e343a"}, {file = "reflink-0.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:be4787c6208faf7fc892390909cf01e34e650ea67c37bf345addefd597ed90e1"}, @@ -2562,6 +2635,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -2583,6 +2657,7 @@ version = "1.12.1" description = "Mock out responses from the requests package" optional = false python-versions = ">=3.5" +groups = ["test"] files = [ {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, @@ -2600,6 +2675,7 @@ version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" +groups = ["main", "test"] files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -2618,6 +2694,8 @@ version = "0.4.3" description = "Efficient signal resampling" optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ {file = "resampy-0.4.3-py3-none-any.whl", hash = "sha256:ad2ed64516b140a122d96704e32bc0f92b23f45419e8b8f478e5a05f83edcebd"}, {file = "resampy-0.4.3.tar.gz", hash = "sha256:a0d1c28398f0e55994b739650afef4e3974115edbe96cd4bb81968425e916e47"}, @@ -2634,13 +2712,14 @@ tests = ["pytest (<8)", "pytest-cov", "scipy (>=1.1)"] [[package]] name = "responses" -version = "0.25.3" +version = "0.25.7" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" +groups = ["test"] files = [ - {file = "responses-0.25.3-py3-none-any.whl", hash = "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb"}, - {file = "responses-0.25.3.tar.gz", hash = "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba"}, + {file = "responses-0.25.7-py3-none-any.whl", hash = "sha256:92ca17416c90fe6b35921f52179bff29332076bb32694c0df02dcac2c6bc043c"}, + {file = "responses-0.25.7.tar.gz", hash = "sha256:8ebae11405d7a5df79ab6fd54277f6f2bc29b2d002d0dd2d5c632594d1ddcedb"}, ] [package.dependencies] @@ -2649,68 +2728,75 @@ requests = ">=2.30.0,<3.0" urllib3 = ">=1.25.10,<3.0" [package.extras] -tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] [[package]] name = "ruff" -version = "0.8.1" +version = "0.11.8" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["lint"] files = [ - {file = "ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5"}, - {file = "ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087"}, - {file = "ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5"}, - {file = "ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790"}, - {file = "ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6"}, - {file = "ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737"}, - {file = "ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f"}, + {file = "ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3"}, + {file = "ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835"}, + {file = "ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458"}, + {file = "ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5"}, + {file = "ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948"}, + {file = "ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb"}, + {file = "ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c"}, + {file = "ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304"}, + {file = "ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2"}, + {file = "ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4"}, + {file = "ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2"}, + {file = "ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8"}, ] [[package]] name = "scikit-learn" -version = "1.5.2" +version = "1.6.1" description = "A set of python modules for machine learning and data mining" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ - {file = "scikit_learn-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6"}, - {file = "scikit_learn-1.5.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0"}, - {file = "scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540"}, - {file = "scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a686885a4b3818d9e62904d91b57fa757fc2bed3e465c8b177be652f4dd37c8"}, - {file = "scikit_learn-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113"}, - {file = "scikit_learn-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445"}, - {file = "scikit_learn-1.5.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de"}, - {file = "scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675"}, - {file = "scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1"}, - {file = "scikit_learn-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6"}, - {file = "scikit_learn-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a"}, - {file = "scikit_learn-1.5.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1"}, - {file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd"}, - {file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6"}, - {file = "scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1"}, - {file = "scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5"}, - {file = "scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908"}, - {file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3"}, - {file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12"}, - {file = "scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f"}, - {file = "scikit_learn-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9"}, - {file = "scikit_learn-1.5.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1"}, - {file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8"}, - {file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca64b3089a6d9b9363cd3546f8978229dcbb737aceb2c12144ee3f70f95684b7"}, - {file = "scikit_learn-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:3bed4909ba187aca80580fe2ef370d9180dcf18e621a27c4cf2ef10d279a7efe"}, - {file = "scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d"}, + {file = "scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e"}, + {file = "scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36"}, + {file = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5"}, + {file = "scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b"}, + {file = "scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002"}, + {file = "scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33"}, + {file = "scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d"}, + {file = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2"}, + {file = "scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8"}, + {file = "scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415"}, + {file = "scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b"}, + {file = "scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2"}, + {file = "scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f"}, + {file = "scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86"}, + {file = "scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52"}, + {file = "scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322"}, + {file = "scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1"}, + {file = "scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348"}, + {file = "scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97"}, + {file = "scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691"}, + {file = "scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f"}, + {file = "scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1"}, + {file = "scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e"}, + {file = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107"}, + {file = "scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422"}, + {file = "scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b"}, + {file = "scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e"}, ] [package.dependencies] @@ -2722,11 +2808,11 @@ threadpoolctl = ">=3.1.0" [package.extras] benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"] build = ["cython (>=3.0.10)", "meson-python (>=0.16.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"] -docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-design (>=0.6.0)", "sphinx-gallery (>=0.16.0)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-design (>=0.6.0)", "sphinx-gallery (>=0.17.1)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)", "towncrier (>=24.8.0)"] examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"] maintenance = ["conda-lock (==2.5.6)"] -tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.2.1)", "scikit-image (>=0.17.2)"] +tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.5.1)", "scikit-image (>=0.17.2)"] [[package]] name = "scipy" @@ -2734,6 +2820,8 @@ version = "1.13.1" description = "Fundamental algorithms for scientific computing in Python" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, @@ -2772,13 +2860,14 @@ test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "po [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "test"] files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -2787,6 +2876,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main", "test"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2798,6 +2888,8 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -2805,13 +2897,15 @@ files = [ [[package]] name = "soco" -version = "0.30.6" +version = "0.30.9" description = "SoCo (Sonos Controller) is a simple library to control Sonos speakers." optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"sonosupdate\"" files = [ - {file = "soco-0.30.6-py2.py3-none-any.whl", hash = "sha256:06c486218d0558a89276ed573ae2264d8e9bfd95a46a7dc253e03d19a3e6f423"}, - {file = "soco-0.30.6.tar.gz", hash = "sha256:7ae48e865dbf1d9fae8023e1b69465c2c4c17048992a05e9c017b35c43d4f4f2"}, + {file = "soco-0.30.9-py2.py3-none-any.whl", hash = "sha256:cf06a56c7431e06fe923dfd58a1217f25e7a1737b74525850859f6d30dc86a24"}, + {file = "soco-0.30.9.tar.gz", hash = "sha256:21f7a3b3f0e65aadfc90aaef69a5a428205597271b09c3d99bea8b5cb00df9da"}, ] [package.dependencies] @@ -2823,40 +2917,41 @@ xmltodict = "*" [package.extras] events-asyncio = ["aiohttp"] -testing = ["black (>=22.12.0)", "coveralls", "flake8", "graphviz", "importlib-metadata (<5)", "pylint", "pytest (>=2.5)", "pytest-cov (<2.6.0)", "requests-mock", "sphinx (==4.5.0)", "sphinx-rtd-theme", "twine", "wheel"] +testing = ["black (>=22.12.0) ; python_version >= \"3.7\"", "coveralls", "flake8", "graphviz", "importlib-metadata (<5) ; python_version == \"3.7\"", "pylint", "pytest (>=2.5)", "pytest-cov (<2.6.0)", "requests-mock", "sphinx (==4.5.0)", "sphinx_rtd_theme", "twine", "wheel"] [[package]] name = "soundfile" -version = "0.12.1" +version = "0.13.1" description = "An audio library based on libsndfile, CFFI and NumPy" optional = true python-versions = "*" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ - {file = "soundfile-0.12.1-py2.py3-none-any.whl", hash = "sha256:828a79c2e75abab5359f780c81dccd4953c45a2c4cd4f05ba3e233ddf984b882"}, - {file = "soundfile-0.12.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d922be1563ce17a69582a352a86f28ed8c9f6a8bc951df63476ffc310c064bfa"}, - {file = "soundfile-0.12.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:bceaab5c4febb11ea0554566784bcf4bc2e3977b53946dda2b12804b4fe524a8"}, - {file = "soundfile-0.12.1-py2.py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:2dc3685bed7187c072a46ab4ffddd38cef7de9ae5eb05c03df2ad569cf4dacbc"}, - {file = "soundfile-0.12.1-py2.py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:074247b771a181859d2bc1f98b5ebf6d5153d2c397b86ee9e29ba602a8dfe2a6"}, - {file = "soundfile-0.12.1-py2.py3-none-win32.whl", hash = "sha256:59dfd88c79b48f441bbf6994142a19ab1de3b9bb7c12863402c2bc621e49091a"}, - {file = "soundfile-0.12.1-py2.py3-none-win_amd64.whl", hash = "sha256:0d86924c00b62552b650ddd28af426e3ff2d4dc2e9047dae5b3d8452e0a49a77"}, - {file = "soundfile-0.12.1.tar.gz", hash = "sha256:e8e1017b2cf1dda767aef19d2fd9ee5ebe07e050d430f77a0a7c66ba08b8cdae"}, + {file = "soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445"}, + {file = "soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33"}, + {file = "soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593"}, + {file = "soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb"}, + {file = "soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618"}, + {file = "soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5"}, + {file = "soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9"}, + {file = "soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b"}, ] [package.dependencies] cffi = ">=1.0" - -[package.extras] -numpy = ["numpy"] +numpy = "*" [[package]] name = "soupsieve" -version = "2.6" +version = "2.7" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" +groups = ["main", "test"] files = [ - {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, - {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, + {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, + {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, ] [[package]] @@ -2865,6 +2960,8 @@ version = "0.5.0.post1" description = "High quality, one-dimensional sample-rate conversion library" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ {file = "soxr-0.5.0.post1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:7406d782d85f8cf64e66b65e6b7721973de8a1dc50b9e88bc2288c343a987484"}, {file = "soxr-0.5.0.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa0a382fb8d8e2afed2c1642723b2d2d1b9a6728ff89f77f3524034c8885b8c9"}, @@ -2902,6 +2999,8 @@ version = "7.4.7" description = "Python documentation generator" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, @@ -2938,6 +3037,8 @@ version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -2954,6 +3055,8 @@ version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -2970,6 +3073,8 @@ version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -2986,6 +3091,8 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = true python-versions = ">=3.5" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -3000,6 +3107,8 @@ version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -3016,6 +3125,8 @@ version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = true python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -3032,6 +3143,7 @@ version = "1.7.0" description = "module to create simple ASCII tables" optional = false python-versions = "*" +groups = ["main", "test"] files = [ {file = "texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917"}, {file = "texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638"}, @@ -3039,13 +3151,15 @@ files = [ [[package]] name = "threadpoolctl" -version = "3.5.0" +version = "3.6.0" description = "threadpoolctl" optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"autobpm\"" files = [ - {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, - {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, + {file = "threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}, + {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, ] [[package]] @@ -3054,6 +3168,7 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["main", "release", "test", "typing"] files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -3091,13 +3206,14 @@ files = [ [[package]] name = "types-beautifulsoup4" -version = "4.12.0.20241020" +version = "4.12.0.20250204" description = "Typing stubs for beautifulsoup4" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["typing"] files = [ - {file = "types-beautifulsoup4-4.12.0.20241020.tar.gz", hash = "sha256:158370d08d0cd448bd11b132a50ff5279237a5d4b5837beba074de152a513059"}, - {file = "types_beautifulsoup4-4.12.0.20241020-py3-none-any.whl", hash = "sha256:c95e66ce15a4f5f0835f7fbc5cd886321ae8294f977c495424eaf4225307fd30"}, + {file = "types_beautifulsoup4-4.12.0.20250204-py3-none-any.whl", hash = "sha256:57ce9e75717b63c390fd789c787d267a67eb01fa6d800a03b9bdde2e877ed1eb"}, + {file = "types_beautifulsoup4-4.12.0.20250204.tar.gz", hash = "sha256:f083d8edcbd01279f8c3995b56cfff2d01f1bb894c3b502ba118d36fbbc495bf"}, ] [package.dependencies] @@ -3105,13 +3221,14 @@ types-html5lib = "*" [[package]] name = "types-flask-cors" -version = "5.0.0.20240902" +version = "5.0.0.20250413" description = "Typing stubs for Flask-Cors" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["typing"] files = [ - {file = "types-Flask-Cors-5.0.0.20240902.tar.gz", hash = "sha256:8921b273bf7cd9636df136b66408efcfa6338a935e5c8f53f5eff1cee03f3394"}, - {file = "types_Flask_Cors-5.0.0.20240902-py3-none-any.whl", hash = "sha256:595e5f36056cd128ab905832e055f2e5d116fbdc685356eea4490bc77df82137"}, + {file = "types_flask_cors-5.0.0.20250413-py3-none-any.whl", hash = "sha256:8183fdba764d45a5b40214468a1d5daa0e86c4ee6042d13f38cc428308f27a64"}, + {file = "types_flask_cors-5.0.0.20250413.tar.gz", hash = "sha256:b346d052f4ef3b606b73faf13e868e458f1efdbfedcbe1aba739eb2f54a6cf5f"}, ] [package.dependencies] @@ -3123,6 +3240,7 @@ version = "1.1.11.20241018" description = "Typing stubs for html5lib" optional = false python-versions = ">=3.8" +groups = ["typing"] files = [ {file = "types-html5lib-1.1.11.20241018.tar.gz", hash = "sha256:98042555ff78d9e3a51c77c918b1041acbb7eb6c405408d8a9e150ff5beccafa"}, {file = "types_html5lib-1.1.11.20241018-py3-none-any.whl", hash = "sha256:3f1e064d9ed2c289001ae6392c84c93833abb0816165c6ff0abfc304a779f403"}, @@ -3130,13 +3248,14 @@ files = [ [[package]] name = "types-mock" -version = "5.1.0.20240425" +version = "5.2.0.20250306" description = "Typing stubs for mock" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["typing"] files = [ - {file = "types-mock-5.1.0.20240425.tar.gz", hash = "sha256:5281a645d72e827d70043e3cc144fe33b1c003db084f789dc203aa90e812a5a4"}, - {file = "types_mock-5.1.0.20240425-py3-none-any.whl", hash = "sha256:d586a01d39ad919d3ddcd73de6cde73ca7f3c69707219f722d1b8d7733641ad7"}, + {file = "types_mock-5.2.0.20250306-py3-none-any.whl", hash = "sha256:eb69fec98b8de26be1d7121623d05a2f117d1ea2e01dd30c123d07d204a15c95"}, + {file = "types_mock-5.2.0.20250306.tar.gz", hash = "sha256:15882cb5cf9980587a7607e31890801223801d7997f559686805ce09b6536087"}, ] [[package]] @@ -3145,6 +3264,7 @@ version = "10.2.0.20240822" description = "Typing stubs for Pillow" optional = false python-versions = ">=3.8" +groups = ["typing"] files = [ {file = "types-Pillow-10.2.0.20240822.tar.gz", hash = "sha256:559fb52a2ef991c326e4a0d20accb3bb63a7ba8d40eb493e0ecb0310ba52f0d3"}, {file = "types_Pillow-10.2.0.20240822-py3-none-any.whl", hash = "sha256:d9dab025aba07aeb12fd50a6799d4eac52a9603488eca09d7662543983f16c5d"}, @@ -3152,24 +3272,26 @@ files = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20240917" +version = "6.0.12.20250402" description = "Typing stubs for PyYAML" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["typing"] files = [ - {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"}, - {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"}, + {file = "types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681"}, + {file = "types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075"}, ] [[package]] name = "types-requests" -version = "2.32.0.20241016" +version = "2.32.0.20250328" description = "Typing stubs for requests" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["typing"] files = [ - {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, - {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, + {file = "types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2"}, + {file = "types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32"}, ] [package.dependencies] @@ -3181,6 +3303,7 @@ version = "1.26.25.14" description = "Typing stubs for urllib3" optional = false python-versions = "*" +groups = ["typing"] files = [ {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, @@ -3188,39 +3311,42 @@ files = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "test", "typing"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] [[package]] name = "unidecode" -version = "1.3.8" +version = "1.4.0" description = "ASCII transliterations of Unicode text" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"}, - {file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"}, + {file = "Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021"}, + {file = "Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23"}, ] [[package]] name = "urllib3" -version = "2.2.3" +version = "2.4.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "test", "typing"] files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, + {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3231,6 +3357,7 @@ version = "3.1.3" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" +groups = ["main", "test", "typing"] files = [ {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, @@ -3248,6 +3375,8 @@ version = "0.14.2" description = "Makes working with XML feel like you are working with JSON" optional = true python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"sonosupdate\"" files = [ {file = "xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac"}, {file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"}, @@ -3259,17 +3388,19 @@ version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" +groups = ["main", "test", "typing"] +markers = "python_version == \"3.9\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] @@ -3300,6 +3431,6 @@ thumbnails = ["Pillow", "pyxdg"] web = ["flask", "flask-cors"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.9,<4" content-hash = "d609e83f7ffeefc12e28d627e5646aa5c1a6f5a56d7013bb649a468069550dba" From 36d42c5b6fde52fa893f902a0ae052a8479660b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 02:54:55 +0100 Subject: [PATCH 022/728] Ensure that pre-commit ruff version is in sync with dependencies --- .pre-commit-config.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d773af3e1..50ee3ec17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,11 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + - repo: local hooks: - - id: ruff-format + - id: format + name: Format + entry: poe format + language: system + files: '.*.py' + pass_filenames: true From c490ac5810b70f3cf5fd8649669838e8fdb19f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 03:06:44 +0100 Subject: [PATCH 023/728] Fix formatting --- beets/test/_common.py | 12 ++++++------ beets/ui/__init__.py | 2 +- beets/ui/commands.py | 4 +--- beets/util/__init__.py | 6 +++--- beetsplug/absubmit.py | 4 ++-- beetsplug/acousticbrainz.py | 2 +- beetsplug/bareasc.py | 2 +- beetsplug/bpm.py | 4 ++-- beetsplug/duplicates.py | 8 ++++---- beetsplug/embedart.py | 2 +- beetsplug/fetchart.py | 2 +- beetsplug/importadded.py | 7 +++---- beetsplug/inline.py | 4 ++-- beetsplug/lastimport.py | 2 +- beetsplug/listenbrainz.py | 2 +- beetsplug/lyrics.py | 14 +++++++------- beetsplug/smartplaylist.py | 2 +- beetsplug/spotify.py | 3 +-- beetsplug/subsonicplaylist.py | 2 +- beetsplug/the.py | 2 +- beetsplug/thumbnails.py | 2 +- beetsplug/zero.py | 2 +- extra/release.py | 2 +- test/plugins/test_convert.py | 18 +++++++++--------- test/plugins/test_embedart.py | 12 ++++++------ test/plugins/test_embyupdate.py | 2 +- test/plugins/test_mbsubmit.py | 2 +- test/plugins/test_play.py | 2 +- test/test_plugins.py | 2 +- test/test_template.py | 6 +++--- test/test_ui.py | 2 +- 31 files changed, 67 insertions(+), 71 deletions(-) diff --git a/beets/test/_common.py b/beets/test/_common.py index 757d461bd..86319c011 100644 --- a/beets/test/_common.py +++ b/beets/test/_common.py @@ -121,15 +121,15 @@ class Assertions: def assertIsFile(self, path): self.assertExists(path) - assert os.path.isfile( - syspath(path) - ), "path exists, but is not a regular file: {!r}".format(path) + assert os.path.isfile(syspath(path)), ( + "path exists, but is not a regular file: {!r}".format(path) + ) def assertIsDir(self, path): self.assertExists(path) - assert os.path.isdir( - syspath(path) - ), "path exists, but is not a directory: {!r}".format(path) + assert os.path.isdir(syspath(path)), ( + "path exists, but is not a directory: {!r}".format(path) + ) def assert_equal_path(self, a, b): """Check that two paths are equal.""" diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 386410a09..8cc5de309 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -1768,7 +1768,7 @@ def _open_library(config): ) ) log.debug( - "library database: {0}\n" "library directory: {1}", + "library database: {0}\nlibrary directory: {1}", util.displayable_path(lib.path), util.displayable_path(lib.directory), ) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 1822c3e7c..12ea4c94d 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1599,9 +1599,7 @@ def list_func(lib, opts, args): list_cmd = ui.Subcommand("list", help="query the library", aliases=("ls",)) -list_cmd.parser.usage += ( - "\n" "Example: %prog -f '$album: $title' artist:beatles" -) +list_cmd.parser.usage += "\nExample: %prog -f '$album: $title' artist:beatles" list_cmd.parser.add_all_common_options() list_cmd.func = list_func default_commands.append(list_cmd) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index ff0a5d273..6d0ce0f88 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -559,7 +559,7 @@ def link(path: bytes, dest: bytes, replace: bool = False): except NotImplementedError: # raised on python >= 3.2 and Windows versions before Vista raise FilesystemError( - "OS does not support symbolic links." "link", + "OS does not support symbolic links.link", (path, dest), traceback.format_exc(), ) @@ -581,14 +581,14 @@ def hardlink(path: bytes, dest: bytes, replace: bool = False): os.link(syspath(path), syspath(dest)) except NotImplementedError: raise FilesystemError( - "OS does not support hard links." "link", + "OS does not support hard links.link", (path, dest), traceback.format_exc(), ) except OSError as exc: if exc.errno == errno.EXDEV: raise FilesystemError( - "Cannot hard link across devices." "link", + "Cannot hard link across devices.link", (path, dest), traceback.format_exc(), ) diff --git a/beetsplug/absubmit.py b/beetsplug/absubmit.py index bbbc14edf..3c48f8897 100644 --- a/beetsplug/absubmit.py +++ b/beetsplug/absubmit.py @@ -157,7 +157,7 @@ only files which would be processed", # If file has no MBID, skip it. if not mbid: self._log.info( - "Not analysing {}, missing " "musicbrainz track id.", item + "Not analysing {}, missing musicbrainz track id.", item ) return None @@ -220,6 +220,6 @@ only files which would be processed", ) else: self._log.debug( - "Successfully submitted AcousticBrainz analysis " "for {}.", + "Successfully submitted AcousticBrainz analysis for {}.", item, ) diff --git a/beetsplug/acousticbrainz.py b/beetsplug/acousticbrainz.py index 899288260..714751ac9 100644 --- a/beetsplug/acousticbrainz.py +++ b/beetsplug/acousticbrainz.py @@ -286,7 +286,7 @@ class AcousticPlugin(plugins.BeetsPlugin): yield v, subdata[k] else: self._log.warning( - "Acousticbrainz did not provide info " "about {}", k + "Acousticbrainz did not provide info about {}", k ) self._log.debug( "Data {} could not be mapped to scheme {} " diff --git a/beetsplug/bareasc.py b/beetsplug/bareasc.py index 0a867dfe1..3a52c41dd 100644 --- a/beetsplug/bareasc.py +++ b/beetsplug/bareasc.py @@ -75,7 +75,7 @@ class BareascPlugin(BeetsPlugin): "bareasc", help="unidecode version of beet list command" ) cmd.parser.usage += ( - "\n" "Example: %prog -f '$album: $title' artist:beatles" + "\nExample: %prog -f '$album: $title' artist:beatles" ) cmd.parser.add_all_common_options() cmd.func = self.unidecode_list diff --git a/beetsplug/bpm.py b/beetsplug/bpm.py index 10edfbfd7..946769cdc 100644 --- a/beetsplug/bpm.py +++ b/beetsplug/bpm.py @@ -57,7 +57,7 @@ class BPMPlugin(BeetsPlugin): def commands(self): cmd = ui.Subcommand( "bpm", - help="determine bpm of a song by pressing " "a key to the rhythm", + help="determine bpm of a song by pressing a key to the rhythm", ) cmd.func = self.command return [cmd] @@ -79,7 +79,7 @@ class BPMPlugin(BeetsPlugin): return self._log.info( - "Press Enter {0} times to the rhythm or Ctrl-D " "to exit", + "Press Enter {0} times to the rhythm or Ctrl-D to exit", self.config["max_strokes"].get(int), ) new_bpm = bpm(self.config["max_strokes"].get(int)) diff --git a/beetsplug/duplicates.py b/beetsplug/duplicates.py index 1e30a60a5..fadb29845 100644 --- a/beetsplug/duplicates.py +++ b/beetsplug/duplicates.py @@ -236,7 +236,7 @@ class DuplicatesPlugin(BeetsPlugin): checksum = getattr(item, key, False) if not checksum: self._log.debug( - "key {0} on item {1} not cached:" "computing checksum", + "key {0} on item {1} not cached:computing checksum", key, displayable_path(item.path), ) @@ -255,7 +255,7 @@ class DuplicatesPlugin(BeetsPlugin): ) else: self._log.debug( - "key {0} on item {1} cached:" "not computing checksum", + "key {0} on item {1} cached:not computing checksum", key, displayable_path(item.path), ) @@ -275,13 +275,13 @@ class DuplicatesPlugin(BeetsPlugin): values = [v for v in values if v not in (None, "")] if strict and len(values) < len(keys): self._log.debug( - "some keys {0} on item {1} are null or empty:" " skipping", + "some keys {0} on item {1} are null or empty: skipping", keys, displayable_path(obj.path), ) elif not strict and not len(values): self._log.debug( - "all keys {0} on item {1} are null or empty:" " skipping", + "all keys {0} on item {1} are null or empty: skipping", keys, displayable_path(obj.path), ) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 740863bf1..2a4e06a93 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -66,7 +66,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): if self.config["maxwidth"].get(int) and not ArtResizer.shared.local: self.config["maxwidth"] = 0 self._log.warning( - "ImageMagick or PIL not found; " "'maxwidth' option ignored" + "ImageMagick or PIL not found; 'maxwidth' option ignored" ) if ( self.config["compare_threshold"].get(int) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 0da884278..cf2200abc 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -686,7 +686,7 @@ class FanartTV(RemoteArtSource): # can this actually occur? else: self._log.debug( - "fanart.tv: unexpected mb_releasegroupid in " "response!" + "fanart.tv: unexpected mb_releasegroupid in response!" ) matches.sort(key=lambda x: int(x["likes"]), reverse=True) diff --git a/beetsplug/importadded.py b/beetsplug/importadded.py index 61a14fba9..2564f26b2 100644 --- a/beetsplug/importadded.py +++ b/beetsplug/importadded.py @@ -58,8 +58,7 @@ class ImportAddedPlugin(BeetsPlugin): or session.config["reflink"] ): self._log.debug( - "In place import detected, recording mtimes from " - "source paths" + "In place import detected, recording mtimes from source paths" ) items = ( [task.item] @@ -95,7 +94,7 @@ class ImportAddedPlugin(BeetsPlugin): mtime = os.stat(util.syspath(source)).st_mtime self.item_mtime[destination] = mtime self._log.debug( - "Recorded mtime {0} for item '{1}' imported from " "'{2}'", + "Recorded mtime {0} for item '{1}' imported from '{2}'", mtime, util.displayable_path(destination), util.displayable_path(source), @@ -130,7 +129,7 @@ class ImportAddedPlugin(BeetsPlugin): def update_item_times(self, lib, item): if self.reimported_item(item): self._log.debug( - "Item '{0}' is reimported, skipping import of " "added date.", + "Item '{0}' is reimported, skipping import of added date.", util.displayable_path(item.path), ) return diff --git a/beetsplug/inline.py b/beetsplug/inline.py index 4092c46d0..c4258fc83 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -28,7 +28,7 @@ class InlineError(Exception): def __init__(self, code, exc): super().__init__( - ("error in inline path field code:\n" "%s\n%s: %s") + ("error in inline path field code:\n%s\n%s: %s") % (code, type(exc).__name__, str(exc)) ) @@ -87,7 +87,7 @@ class InlinePlugin(BeetsPlugin): func = _compile_func(python_code) except SyntaxError: self._log.error( - "syntax error in inline field definition:\n" "{0}", + "syntax error in inline field definition:\n{0}", traceback.format_exc(), ) return diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index f59205b99..28cf958a5 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -267,7 +267,7 @@ def process_tracks(lib, tracks, log): count = int(song.get("play_count", 0)) new_count = int(tracks[num].get("playcount", 1)) log.debug( - "match: {0} - {1} ({2}) " "updating: play_count {3} => {4}", + "match: {0} - {1} ({2}) updating: play_count {3} => {4}", song.artist, song.title, song.album, diff --git a/beetsplug/listenbrainz.py b/beetsplug/listenbrainz.py index 37a7920b9..c579645db 100644 --- a/beetsplug/listenbrainz.py +++ b/beetsplug/listenbrainz.py @@ -180,7 +180,7 @@ class ListenBrainzPlugin(BeetsPlugin): ) for playlist in listenbrainz_playlists: self._log.debug( - f'Playlist: {playlist["type"]} - {playlist["date"]}' + f"Playlist: {playlist['type']} - {playlist['date']}" ) return listenbrainz_playlists diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index cb48e2424..3e979221c 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -557,7 +557,7 @@ class Genius(SearchBackend): @cached_property def headers(self) -> dict[str, str]: - return {"Authorization": f'Bearer {self.config["genius_api_key"]}'} + return {"Authorization": f"Bearer {self.config['genius_api_key']}"} def search(self, artist: str, title: str) -> Iterable[SearchResult]: search_data: GeniusAPI.Search = self.fetch_json( @@ -913,17 +913,17 @@ class RestFiles: def write_artist(self, artist: str, items: Iterable[Item]) -> None: parts = [ - f'{artist}\n{"=" * len(artist)}', + f"{artist}\n{'=' * len(artist)}", ".. contents::\n :local:", ] for album, items in groupby(items, key=lambda i: i.album): - parts.append(f'{album}\n{"-" * len(album)}') + parts.append(f"{album}\n{'-' * len(album)}") parts.extend( part for i in items if (title := f":index:`{i.title.strip()}`") for part in ( - f'{title}\n{"~" * len(title)}', + f"{title}\n{'~' * len(title)}", textwrap.indent(i.lyrics, "| "), ) ) @@ -941,9 +941,9 @@ class RestFiles: d = self.directory text = f""" ReST files generated. to build, use one of: - sphinx-build -b html {d} {d/"html"} - sphinx-build -b epub {d} {d/"epub"} - sphinx-build -b latex {d} {d/"latex"} && make -C {d/"latex"} all-pdf + sphinx-build -b html {d} {d / "html"} + sphinx-build -b epub {d} {d / "epub"} + sphinx-build -b latex {d} {d / "latex"} && make -C {d / "latex"} all-pdf """ ui.print_(textwrap.dedent(text)) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index d758c0255..5ea3c6bff 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -327,7 +327,7 @@ class SmartPlaylistPlugin(BeetsPlugin): if extm3u: attr = [(k, entry.item[k]) for k in keys] al = [ - f" {key}=\"{quote(str(value), safe='/:')}\"" + f' {key}="{quote(str(value), safe="/:")}"' for key, value in attr ] attrs = "".join(al) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 55a77a8a7..3f5b16d11 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -208,8 +208,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): "Retry-After", DEFAULT_WAITING_TIME ) self._log.debug( - f"Too many API requests. Retrying after " - f"{seconds} seconds." + f"Too many API requests. Retrying after {seconds} seconds." ) time.sleep(int(seconds) + 1) return self._handle_response( diff --git a/beetsplug/subsonicplaylist.py b/beetsplug/subsonicplaylist.py index 606cdc8bd..9b4a7778c 100644 --- a/beetsplug/subsonicplaylist.py +++ b/beetsplug/subsonicplaylist.py @@ -115,7 +115,7 @@ class SubsonicPlaylistPlugin(BeetsPlugin): )[0] if playlists.attrib.get("code", "200") != "200": alt_error = ( - "error getting playlists," " but no error message found" + "error getting playlists, but no error message found" ) self._log.warn(playlists.attrib.get("message", alt_error)) return diff --git a/beetsplug/the.py b/beetsplug/the.py index 42da708a3..802b0a3db 100644 --- a/beetsplug/the.py +++ b/beetsplug/the.py @@ -54,7 +54,7 @@ class ThePlugin(BeetsPlugin): else: if not (p.startswith("^") or p.endswith("$")): self._log.warning( - 'warning: "{0}" will not ' "match string start/end", + 'warning: "{0}" will not match string start/end', p, ) if self.config["a"]: diff --git a/beetsplug/thumbnails.py b/beetsplug/thumbnails.py index f0755c0f9..e11b75390 100644 --- a/beetsplug/thumbnails.py +++ b/beetsplug/thumbnails.py @@ -161,7 +161,7 @@ class ThumbnailsPlugin(BeetsPlugin): ) else: self._log.debug( - "{1}x{1} thumbnail for {0} exists and is " "recent enough", + "{1}x{1} thumbnail for {0} exists and is recent enough", album, size, ) diff --git a/beetsplug/zero.py b/beetsplug/zero.py index bda4052ab..5d1244dec 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -93,7 +93,7 @@ class ZeroPlugin(BeetsPlugin): self._log.error("invalid field: {0}", field) elif field in ("id", "path", "album_id"): self._log.warning( - "field '{0}' ignored, zeroing " "it would be dangerous", field + "field '{0}' ignored, zeroing it would be dangerous", field ) else: try: diff --git a/extra/release.py b/extra/release.py index 1891f17f2..16e2e860e 100755 --- a/extra/release.py +++ b/extra/release.py @@ -170,7 +170,7 @@ For packagers: Other changes: {new_header} -{'-' * len(new_header)} +{"-" * len(new_header)} """, text, ) diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index a2b4eaf67..6dd28337a 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -67,9 +67,9 @@ class ConvertMixin: self.assertIsFile(path) with open(path, "rb") as f: f.seek(-len(display_tag), os.SEEK_END) - assert ( - f.read() == tag - ), f"{displayable_path(path)} is not tagged with {display_tag}" + assert f.read() == tag, ( + f"{displayable_path(path)} is not tagged with {display_tag}" + ) def assertNoFileTag(self, path, tag): """Assert that the path is a file and the files content does not @@ -80,9 +80,9 @@ class ConvertMixin: self.assertIsFile(path) with open(path, "rb") as f: f.seek(-len(tag), os.SEEK_END) - assert ( - f.read() != tag - ), f"{displayable_path(path)} is unexpectedly tagged with {display_tag}" + assert f.read() != tag, ( + f"{displayable_path(path)} is unexpectedly tagged with {display_tag}" + ) class ConvertTestCase(ConvertMixin, PluginTestCase): @@ -124,9 +124,9 @@ class ImportConvertTest(AsIsImporterMixin, ImportHelper, ConvertTestCase): self.run_asis_importer() for path in self.importer.paths: for root, dirnames, filenames in os.walk(path): - assert ( - len(fnmatch.filter(filenames, "*.mp3")) == 0 - ), f"Non-empty import directory {util.displayable_path(path)}" + assert len(fnmatch.filter(filenames, "*.mp3")) == 0, ( + f"Non-empty import directory {util.displayable_path(path)}" + ) def get_count_of_import_files(self): import_file_count = 0 diff --git a/test/plugins/test_embedart.py b/test/plugins/test_embedart.py index 2d2d68153..cb4d1a421 100644 --- a/test/plugins/test_embedart.py +++ b/test/plugins/test_embedart.py @@ -153,9 +153,9 @@ class EmbedartCliTest(PluginMixin, FetchImageHelper, BeetsTestCase): self.run_command("embedart", "-y", "-f", self.abbey_differentpath) mediafile = MediaFile(syspath(item.path)) - assert ( - mediafile.images[0].data == self.image_data - ), f"Image written is not {displayable_path(self.abbey_artpath)}" + assert mediafile.images[0].data == self.image_data, ( + f"Image written is not {displayable_path(self.abbey_artpath)}" + ) @require_artresizer_compare def test_accept_similar_art(self): @@ -167,9 +167,9 @@ class EmbedartCliTest(PluginMixin, FetchImageHelper, BeetsTestCase): self.run_command("embedart", "-y", "-f", self.abbey_similarpath) mediafile = MediaFile(syspath(item.path)) - assert ( - mediafile.images[0].data == self.image_data - ), f"Image written is not {displayable_path(self.abbey_similarpath)}" + assert mediafile.images[0].data == self.image_data, ( + f"Image written is not {displayable_path(self.abbey_similarpath)}" + ) def test_non_ascii_album_path(self): resource_path = os.path.join(_common.RSRC, b"image.mp3") diff --git a/test/plugins/test_embyupdate.py b/test/plugins/test_embyupdate.py index 8def5dca5..9c7104371 100644 --- a/test/plugins/test_embyupdate.py +++ b/test/plugins/test_embyupdate.py @@ -143,7 +143,7 @@ class EmbyUpdateTest(PluginTestCase): responses.add( responses.POST, - ("http://localhost:8096" "/Users/AuthenticateByName"), + ("http://localhost:8096/Users/AuthenticateByName"), body=body, status=200, content_type="application/json", diff --git a/test/plugins/test_mbsubmit.py b/test/plugins/test_mbsubmit.py index f92d85973..04b1b736e 100644 --- a/test/plugins/test_mbsubmit.py +++ b/test/plugins/test_mbsubmit.py @@ -64,6 +64,6 @@ class MBSubmitPluginTest(PluginMixin, TerminalImportMixin, ImportTestCase): # Manually build the string for comparing the output. tracklist = ( - "Open files with Picard? " "02. Tag Track 2 - Tag Artist (0:01)" + "Open files with Picard? 02. Tag Track 2 - Tag Artist (0:01)" ) assert tracklist in output.getvalue() diff --git a/test/plugins/test_play.py b/test/plugins/test_play.py index 712739633..571af95dd 100644 --- a/test/plugins/test_play.py +++ b/test/plugins/test_play.py @@ -97,7 +97,7 @@ class PlayPluginTest(CleanupModulesMixin, PluginTestCase): with open(open_mock.call_args[0][0][0], "rb") as f: playlist = f.read().decode("utf-8") assert ( - f'{os.path.dirname(self.item.path.decode("utf-8"))}\n' == playlist + f"{os.path.dirname(self.item.path.decode('utf-8'))}\n" == playlist ) def test_raw(self, open_mock): diff --git a/test/test_plugins.py b/test/test_plugins.py index efa26d084..d273de698 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -174,7 +174,7 @@ class EventsTest(PluginImportTestCase): logs = [line for line in logs if not line.startswith("Sending event:")] assert logs == [ - f'Album: {displayable_path(os.path.join(self.import_dir, b"album"))}', + f"Album: {displayable_path(os.path.join(self.import_dir, b'album'))}", f" {displayable_path(self.import_media[0].path)}", f" {displayable_path(self.import_media[1].path)}", ] diff --git a/test/test_template.py b/test/test_template.py index 236bee5aa..031aab289 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -61,9 +61,9 @@ class ParseTest(unittest.TestCase): """ assert isinstance(obj, functemplate.Call), f"not a Call: {obj}" assert obj.ident == ident, f"wrong identifier: {obj.ident} vs. {ident}" - assert ( - len(obj.args) == numargs - ), f"wrong argument count in {obj.ident}: {len(obj.args)} vs. {numargs}" + assert len(obj.args) == numargs, ( + f"wrong argument count in {obj.ident}: {len(obj.args)} vs. {numargs}" + ) def test_plain_text(self): assert list(_normparse("hello world")) == ["hello world"] diff --git a/test/test_ui.py b/test/test_ui.py index e9588dbc6..afa16e171 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -1441,7 +1441,7 @@ class CompletionTest(TestPluginTestCase): assert tester.returncode == 0 assert out == b"completion tests passed\n", ( "test/test_completion.sh did not execute properly. " - f'Output:{out.decode("utf-8")}' + f"Output:{out.decode('utf-8')}" ) From 6869afd7fe8d0fd9ae2843d8342dd8f75cfe0799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 10:41:58 +0100 Subject: [PATCH 024/728] Exclude formatting commit from blame --- .git-blame-ignore-revs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 12d5ffd94..8848bf384 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -43,3 +43,7 @@ a6e5201ff3fad4c69bf24d17bace2ef744b9f51b f36bc497c8c8f89004f3f6879908d3f0b25123e1 # Remove some lint exclusions and fix the issues 5f78d1b82b2292d5ce0c99623ba0ec444b80d24c + +# 2025 +# Fix formatting +c490ac5810b70f3cf5fd8649669838e8fdb19f4d From 99dc0861c25008fe85620bc94274e537c18aeaf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 06:55:04 +0100 Subject: [PATCH 025/728] Redact sensitive fields Redacted fields reported by GitHub secrets scanner[1] and a couple of others. 1: https://github.com/beetbox/beets/security/secret-scanning?query=is%3Aclosed --- beetsplug/embyupdate.py | 25 +++++++++++++++---------- beetsplug/fetchart.py | 1 + beetsplug/kodiupdate.py | 10 +++++----- beetsplug/lastimport.py | 1 + beetsplug/spotify.py | 1 + beetsplug/subsonicupdate.py | 32 +++++++++++++++----------------- 6 files changed, 38 insertions(+), 32 deletions(-) diff --git a/beetsplug/embyupdate.py b/beetsplug/embyupdate.py index 2cda6af5e..c696f39f3 100644 --- a/beetsplug/embyupdate.py +++ b/beetsplug/embyupdate.py @@ -13,7 +13,6 @@ from urllib.parse import parse_qs, urlencode, urljoin, urlsplit, urlunsplit import requests -from beets import config from beets.plugins import BeetsPlugin @@ -143,17 +142,23 @@ def get_user(host, port, username): class EmbyUpdate(BeetsPlugin): def __init__(self): - super().__init__() + super().__init__("emby") # Adding defaults. - config["emby"].add( + self.config.add( { "host": "http://localhost", "port": 8096, - "apikey": None, + "username": None, "password": None, + "userid": None, + "apikey": None, } ) + self.config["username"].redact = True + self.config["password"].redact = True + self.config["userid"].redact = True + self.config["apikey"].redact = True self.register_listener("database_change", self.listen_for_db_change) @@ -165,12 +170,12 @@ class EmbyUpdate(BeetsPlugin): """When the client exists try to send refresh request to Emby.""" self._log.info("Updating Emby library...") - host = config["emby"]["host"].get() - port = config["emby"]["port"].get() - username = config["emby"]["username"].get() - password = config["emby"]["password"].get() - userid = config["emby"]["userid"].get() - token = config["emby"]["apikey"].get() + host = self.config["host"].get() + port = self.config["port"].get() + username = self.config["username"].get() + password = self.config["password"].get() + userid = self.config["userid"].get() + token = self.config["apikey"].get() # Check if at least a apikey or password is given. if not any([password, token]): diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index cf2200abc..a1bd26055 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -566,6 +566,7 @@ class GoogleImages(RemoteArtSource): } ) config["google_key"].redact = True + config["google_engine"].redact = True @classmethod def available(cls, log, config): diff --git a/beetsplug/kodiupdate.py b/beetsplug/kodiupdate.py index d5d699537..2f679c38b 100644 --- a/beetsplug/kodiupdate.py +++ b/beetsplug/kodiupdate.py @@ -25,7 +25,6 @@ Put something like the following in your config.yaml to configure: import requests -from beets import config from beets.plugins import BeetsPlugin @@ -53,14 +52,15 @@ def update_kodi(host, port, user, password): class KodiUpdate(BeetsPlugin): def __init__(self): - super().__init__() + super().__init__("kodi") # Adding defaults. - config["kodi"].add( + self.config.add( [{"host": "localhost", "port": 8080, "user": "kodi", "pwd": "kodi"}] ) - config["kodi"]["pwd"].redact = True + self.config["user"].redact = True + self.config["pwd"].redact = True self.register_listener("database_change", self.listen_for_db_change) def listen_for_db_change(self, lib, model): @@ -71,7 +71,7 @@ class KodiUpdate(BeetsPlugin): """When the client exists try to send refresh request to Kodi server.""" self._log.info("Requesting a Kodi library update...") - kodi = config["kodi"].get() + kodi = self.config.get() # Backwards compatibility in case not configured as an array if not isinstance(kodi, list): diff --git a/beetsplug/lastimport.py b/beetsplug/lastimport.py index 28cf958a5..122e5f9cd 100644 --- a/beetsplug/lastimport.py +++ b/beetsplug/lastimport.py @@ -31,6 +31,7 @@ class LastImportPlugin(plugins.BeetsPlugin): "api_key": plugins.LASTFM_KEY, } ) + config["lastfm"]["user"].redact = True config["lastfm"]["api_key"].redact = True self.config.add( { diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 3f5b16d11..44a0e0ce7 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -105,6 +105,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): "tokenfile": "spotify_token.json", } ) + self.config["client_id"].redact = True self.config["client_secret"].redact = True self.tokenfile = self.config["tokenfile"].get( diff --git a/beetsplug/subsonicupdate.py b/beetsplug/subsonicupdate.py index 2a537e35f..ce888cb76 100644 --- a/beetsplug/subsonicupdate.py +++ b/beetsplug/subsonicupdate.py @@ -36,7 +36,6 @@ from binascii import hexlify import requests -from beets import config from beets.plugins import BeetsPlugin __author__ = "https://github.com/maffo999" @@ -44,9 +43,9 @@ __author__ = "https://github.com/maffo999" class SubsonicUpdate(BeetsPlugin): def __init__(self): - super().__init__() + super().__init__("subsonic") # Set default configuration values - config["subsonic"].add( + self.config.add( { "user": "admin", "pass": "admin", @@ -54,7 +53,8 @@ class SubsonicUpdate(BeetsPlugin): "auth": "token", } ) - config["subsonic"]["pass"].redact = True + self.config["user"].redact = True + self.config["pass"].redact = True self.register_listener("database_change", self.db_change) self.register_listener("smartplaylist_update", self.spl_update) @@ -64,13 +64,12 @@ class SubsonicUpdate(BeetsPlugin): def spl_update(self): self.register_listener("cli_exit", self.start_scan) - @staticmethod - def __create_token(): + def __create_token(self): """Create salt and token from given password. :return: The generated salt and hashed token """ - password = config["subsonic"]["pass"].as_str() + password = self.config["pass"].as_str() # Pick the random sequence and salt the password r = string.ascii_letters + string.digits @@ -81,8 +80,7 @@ class SubsonicUpdate(BeetsPlugin): # Put together the payload of the request to the server and the URL return salt, token - @staticmethod - def __format_url(endpoint): + def __format_url(self, endpoint): """Get the Subsonic URL to trigger the given endpoint. Uses either the url config option or the deprecated host, port, and context_path config options together. @@ -90,15 +88,15 @@ class SubsonicUpdate(BeetsPlugin): :return: Endpoint for updating Subsonic """ - url = config["subsonic"]["url"].as_str() + url = self.config["url"].as_str() if url and url.endswith("/"): url = url[:-1] # @deprecated("Use url config option instead") if not url: - host = config["subsonic"]["host"].as_str() - port = config["subsonic"]["port"].get(int) - context_path = config["subsonic"]["contextpath"].as_str() + host = self.config["host"].as_str() + port = self.config["port"].get(int) + context_path = self.config["contextpath"].as_str() if context_path == "/": context_path = "" url = f"http://{host}:{port}{context_path}" @@ -106,11 +104,11 @@ class SubsonicUpdate(BeetsPlugin): return url + f"/rest/{endpoint}" def start_scan(self): - user = config["subsonic"]["user"].as_str() - auth = config["subsonic"]["auth"].as_str() + user = self.config["user"].as_str() + auth = self.config["auth"].as_str() url = self.__format_url("startScan") self._log.debug("URL is {0}", url) - self._log.debug("auth type is {0}", config["subsonic"]["auth"]) + self._log.debug("auth type is {0}", self.config["auth"]) if auth == "token": salt, token = self.__create_token() @@ -123,7 +121,7 @@ class SubsonicUpdate(BeetsPlugin): "f": "json", } elif auth == "password": - password = config["subsonic"]["pass"].as_str() + password = self.config["pass"].as_str() encpass = hexlify(password.encode()).decode() payload = { "u": user, From fdc1aba60323af50bc55702df49f72dbfffa84cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 12:49:58 +0100 Subject: [PATCH 026/728] Replace typing.cast with explicit type definitions and ignore TC006 --- beets/autotag/hooks.py | 3 +-- beets/autotag/match.py | 33 +++++++++++++++++---------------- beets/autotag/mb.py | 23 +++++++++++++++-------- beets/dbcore/db.py | 6 +++--- beetsplug/replaygain.py | 12 +++++------- pyproject.toml | 4 ++++ 6 files changed, 45 insertions(+), 36 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 81cfd7bb2..33606020d 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -18,7 +18,7 @@ from __future__ import annotations import re from functools import total_ordering -from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, cast +from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar from jellyfish import levenshtein_distance from unidecode import unidecode @@ -474,7 +474,6 @@ class Distance: matched against `value2`. """ if isinstance(value1, re.Pattern): - value2 = cast(str, value2) return bool(value1.match(value2)) return value1 == value2 diff --git a/beets/autotag/match.py b/beets/autotag/match.py index bc30ccea2..433093def 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -20,10 +20,9 @@ from __future__ import annotations import datetime import re -from collections.abc import Iterable, Sequence from enum import IntEnum from functools import cache -from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar, cast +from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar import lap import numpy as np @@ -40,6 +39,8 @@ from beets.autotag import ( from beets.util import plurality if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + from beets.library import Item # Artist signals that indicate "various artists". These are used at the @@ -241,12 +242,14 @@ def distance( # Album. dist.add_string("album", likelies["album"], album_info.album) + preferred_config = config["match"]["preferred"] # Current or preferred media. if album_info.media: # Preferred media options. - patterns = config["match"]["preferred"]["media"].as_str_seq() - patterns = cast(Sequence[str], patterns) - options = [re.compile(r"(\d+x)?(%s)" % pat, re.I) for pat in patterns] + media_patterns: Sequence[str] = preferred_config["media"].as_str_seq() + options = [ + re.compile(r"(\d+x)?(%s)" % pat, re.I) for pat in media_patterns + ] if options: dist.add_priority("media", album_info.media, options) # Current media. @@ -258,7 +261,7 @@ def distance( dist.add_number("mediums", likelies["disctotal"], album_info.mediums) # Prefer earliest release. - if album_info.year and config["match"]["preferred"]["original_year"]: + if album_info.year and preferred_config["original_year"]: # Assume 1889 (earliest first gramophone discs) if we don't know the # original year. original = album_info.original_year or 1889 @@ -282,9 +285,8 @@ def distance( dist.add("year", 1.0) # Preferred countries. - patterns = config["match"]["preferred"]["countries"].as_str_seq() - patterns = cast(Sequence[str], patterns) - options = [re.compile(pat, re.I) for pat in patterns] + country_patterns: Sequence[str] = preferred_config["countries"].as_str_seq() + options = [re.compile(pat, re.I) for pat in country_patterns] if album_info.country and options: dist.add_priority("country", album_info.country, options) # Country. @@ -447,9 +449,8 @@ def _add_candidate( return # Discard matches without required tags. - for req_tag in cast( - Sequence[str], config["match"]["required"].as_str_seq() - ): + required_tags: Sequence[str] = config["match"]["required"].as_str_seq() + for req_tag in required_tags: if getattr(info, req_tag) is None: log.debug("Ignored. Missing required tag: {0}", req_tag) return @@ -462,8 +463,8 @@ def _add_candidate( # Skip matches with ignored penalties. penalties = [key for key, _ in dist] - ignored = cast(Sequence[str], config["match"]["ignored"].as_str_seq()) - for penalty in ignored: + ignored_tags: Sequence[str] = config["match"]["ignored"].as_str_seq() + for penalty in ignored_tags: if penalty in penalties: log.debug("Ignored. Penalty: {0}", penalty) return @@ -499,8 +500,8 @@ def tag_album( """ # Get current metadata. likelies, consensus = current_metadata(items) - cur_artist = cast(str, likelies["artist"]) - cur_album = cast(str, likelies["album"]) + cur_artist: str = likelies["artist"] + cur_album: str = likelies["album"] log.debug("Tagging {0} - {1}", cur_artist, cur_album) # The output result, keys are the MB album ID. diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 6c2b604cd..28cb66ca1 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -19,9 +19,8 @@ from __future__ import annotations import re import traceback from collections import Counter -from collections.abc import Iterator, Sequence from itertools import product -from typing import Any, cast +from typing import TYPE_CHECKING, Any from urllib.parse import urljoin import musicbrainzngs @@ -37,6 +36,9 @@ from beets.util.id_extractors import ( spotify_id_regex, ) +if TYPE_CHECKING: + from collections.abc import Iterator, Sequence + VARIOUS_ARTISTS_ID = "89ad4ac3-39f7-470e-963a-56509c546377" BASE_URL = "https://musicbrainz.org/" @@ -178,15 +180,18 @@ def _preferred_alias(aliases: list): return matches[0] -def _preferred_release_event(release: dict[str, Any]) -> tuple[str, str]: +def _preferred_release_event( + release: dict[str, Any], +) -> tuple[str | None, str | None]: """Given a release, select and return the user's preferred release event as a tuple of (country, release_date). Fall back to the default release event if a preferred event is not found. """ - countries = config["match"]["preferred"]["countries"].as_str_seq() - countries = cast(Sequence, countries) + preferred_countries: Sequence[str] = config["match"]["preferred"][ + "countries" + ].as_str_seq() - for country in countries: + for country in preferred_countries: for event in release.get("release-event-list", {}): try: if country in event["area"]["iso-3166-1-code-list"]: @@ -194,7 +199,7 @@ def _preferred_release_event(release: dict[str, Any]) -> tuple[str, str]: except KeyError: pass - return (cast(str, release.get("country")), cast(str, release.get("date"))) + return release.get("country"), release.get("date") def _multi_artist_credit( @@ -589,7 +594,9 @@ def album_info(release: dict) -> beets.autotag.hooks.AlbumInfo: if not release_date: # Fall back if release-specific date is not available. release_date = release_group_date - _set_date_str(info, release_date, False) + + if release_date: + _set_date_str(info, release_date, False) _set_date_str(info, release_group_date, True) # Label name. diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index dd8401935..16ca54995 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -26,7 +26,7 @@ from abc import ABC from collections import defaultdict from collections.abc import Generator, Iterable, Iterator, Mapping, Sequence from sqlite3 import Connection -from typing import TYPE_CHECKING, Any, AnyStr, Callable, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, AnyStr, Callable, Generic, TypeVar from unidecode import unidecode @@ -126,8 +126,8 @@ class FormattedMapping(Mapping[str, str]): value = value.decode("utf-8", "ignore") if self.for_path: - sep_repl = cast(str, beets.config["path_sep_replace"].as_str()) - sep_drive = cast(str, beets.config["drive_sep_replace"].as_str()) + sep_repl: str = beets.config["path_sep_replace"].as_str() + sep_drive: str = beets.config["drive_sep_replace"].as_str() if re.match(r"^\w:", value): value = re.sub(r"(?<=^\w):", sep_drive, value) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 5ee9aa486..3aad8cd89 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -28,7 +28,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from multiprocessing.pool import ThreadPool from threading import Event, Thread -from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast +from typing import TYPE_CHECKING, Any, Callable, TypeVar from beets import ui from beets.plugins import BeetsPlugin @@ -576,7 +576,7 @@ class CommandBackend(Backend): } ) - self.command = cast(str, config["command"].as_str()) + self.command: str = config["command"].as_str() if self.command: # Explicit executable path. @@ -1225,7 +1225,7 @@ class ReplayGainPlugin(BeetsPlugin): # FIXME: Consider renaming the configuration option and deprecating the # old name 'overwrite'. - self.force_on_import = cast(bool, self.config["overwrite"].get(bool)) + self.force_on_import: bool = self.config["overwrite"].get(bool) # Remember which backend is used for CLI feedback self.backend_name = self.config["backend"].as_str() @@ -1491,7 +1491,7 @@ class ReplayGainPlugin(BeetsPlugin): def import_begin(self, session: ImportSession): """Handle `import_begin` event -> open pool""" - threads = cast(int, self.config["threads"].get(int)) + threads: int = self.config["threads"].get(int) if ( self.config["parallel_on_import"] @@ -1526,9 +1526,7 @@ class ReplayGainPlugin(BeetsPlugin): # Bypass self.open_pool() if called with `--threads 0` if opts.threads != 0: - threads = opts.threads or cast( - int, self.config["threads"].get(int) - ) + threads: int = opts.threads or self.config["threads"].get(int) self.open_pool(threads) if opts.album: diff --git a/pyproject.toml b/pyproject.toml index 8f4403f20..ab2c42267 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -263,6 +263,10 @@ select = [ "TCH", # flake8-type-checking "W", # pycodestyle ] +ignore = [ + "TC006" # no need to quote 'cast's since we use 'from __future__ import annotations' +] + [tool.ruff.lint.per-file-ignores] "beets/**" = ["PT"] "test/test_util.py" = ["E501"] From a735e747f8d45d66ceb81315b6984d8e51c0c16a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 10:40:55 +0100 Subject: [PATCH 027/728] Skip tekstowo lyrics test --- test/plugins/lyrics_pages.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/plugins/lyrics_pages.py b/test/plugins/lyrics_pages.py index 2d681e111..ef2eeb1a2 100644 --- a/test/plugins/lyrics_pages.py +++ b/test/plugins/lyrics_pages.py @@ -605,5 +605,6 @@ lyrics_pages = [ Children at your feet Wonder how you manage to make ends meet """, + marks=[pytest.mark.xfail(reason="Tekstowo seems to be broken again")], ), ] From 250b0da900c095a09bedc1a0717381010c01a3c6 Mon Sep 17 00:00:00 2001 From: snejus <snejus@users.noreply.github.com> Date: Wed, 7 May 2025 22:34:25 +0000 Subject: [PATCH 028/728] Increment version to 2.3.0 --- beets/__init__.py | 2 +- docs/changelog.rst | 11 +++++++++++ docs/conf.py | 4 ++-- pyproject.toml | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 845d251ae..1bac81b65 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -17,7 +17,7 @@ from sys import stderr import confuse -__version__ = "2.2.0" +__version__ = "2.3.0" __author__ = "Adrian Sampson <adrian@radbox.org>" diff --git a/docs/changelog.rst b/docs/changelog.rst index 1ab7229e7..9dd129712 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,17 @@ Changelog goes here! Please add your entry to the bottom of one of the lists bel Unreleased ---------- +New features: + +Bug fixes: + +For packagers: + +Other changes: + +2.3.0 (May 07, 2025) +-------------------- + Beets now requires Python 3.9 or later since support for EOL Python 3.8 has been dropped. diff --git a/docs/conf.py b/docs/conf.py index 337a76a54..fafabef70 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,8 +11,8 @@ master_doc = "index" project = "beets" copyright = "2016, Adrian Sampson" -version = "2.2" -release = "2.2.0" +version = "2.3" +release = "2.3.0" pygments_style = "sphinx" diff --git a/pyproject.toml b/pyproject.toml index ab2c42267..f83c174b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "beets" -version = "2.2.0" +version = "2.3.0" description = "music tagger and library organizer" authors = ["Adrian Sampson <adrian@radbox.org>"] maintainers = ["Serene-Arc"] From 677204238c2a56bd09ff1cef34d9c4ff919c0e12 Mon Sep 17 00:00:00 2001 From: jwynn6 <6757260+jwynn6@users.noreply.github.com> Date: Sat, 10 May 2025 20:55:55 -0400 Subject: [PATCH 029/728] Update pathformat.rst (#5754) Added explainer about escaping closing braces `$}` --- docs/reference/pathformat.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/pathformat.rst b/docs/reference/pathformat.rst index d80bdec34..d89eb6767 100644 --- a/docs/reference/pathformat.rst +++ b/docs/reference/pathformat.rst @@ -173,7 +173,7 @@ write a function call. To escape any of these characters (except ``{``, and * ``${``, which is ambiguous with the variable reference syntax (like ``${title}``). To insert a ``{`` alone, it's always sufficient to just type - ``{``. + ``{``. You do, however need to use ``$`` to escape a closing brace ``$}``. * commas are used as argument separators in function calls. Inside of a function's argument, use ``$,`` to get a literal ``,`` character. Outside of any function argument, escaping is not necessary: ``,`` by itself will From de09c3217aa54672806e84f1e3a09c22974f6d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 11 May 2025 01:35:47 +0100 Subject: [PATCH 030/728] Do not 'legalize' paths by removing everything following a dot TIL that `with_suffix` does not simply append the suffix to the filename - it instead replaces the old/current suffix. Or whatever seems to look like a suffix, in our case, unfortunately... --- beets/util/__init__.py | 2 +- docs/changelog.rst | 5 +++++ test/test_util.py | 18 +++++++++++------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 6d0ce0f88..e17de1f51 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -715,7 +715,7 @@ def truncate_path(str_path: str) -> str: path = Path(str_path) parent_parts = [truncate_str(p, max_length) for p in path.parts[:-1]] stem = truncate_str(path.stem, max_length - len(path.suffix)) - return str(Path(*parent_parts, stem).with_suffix(path.suffix)) + return str(Path(*parent_parts, stem)) + path.suffix def _legalize_stage( diff --git a/docs/changelog.rst b/docs/changelog.rst index 9dd129712..ab0b9519d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,11 @@ New features: Bug fixes: +* :doc:`/reference/pathformat`: Fixed a regression where path legalization + incorrectly removed parts of user-configured path formats that followed a dot + (**.**). + :bug:`5771` + For packagers: Other changes: diff --git a/test/test_util.py b/test/test_util.py index 3a5e55c49..6b795b957 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -171,6 +171,8 @@ class PathConversionTest(BeetsTestCase): class TestPathLegalization: + _p = pytest.param + @pytest.fixture(autouse=True) def _patch_max_filename_length(self, monkeypatch): monkeypatch.setattr("beets.util.get_max_filename_length", lambda: 5) @@ -178,20 +180,22 @@ class TestPathLegalization: @pytest.mark.parametrize( "path, expected", [ - ("abcdeX/fgh", "abcde/fgh"), - ("abcde/fXX.ext", "abcde/f.ext"), - ("a🎹/a.ext", "a🎹/a.ext"), - ("ab🎹/a.ext", "ab/a.ext"), + _p("abcdeX/fgh", "abcde/fgh", id="truncate-parent-dir"), + _p("abcde/fXX.ext", "abcde/f.ext", id="truncate-filename"), + # note that 🎹 is 4 bytes long: + # >>> "🎹".encode("utf-8") + # b'\xf0\x9f\x8e\xb9' + _p("a🎹/a.ext", "a🎹/a.ext", id="unicode-fit"), + _p("ab🎹/a.ext", "ab/a.ext", id="unicode-truncate-fully-one-byte-over-limit"), + _p("f.a.e", "f.a.e", id="persist-dot-in-filename"), # see #5771 ], - ) + ) # fmt: skip def test_truncate(self, path, expected): path = path.replace("/", os.path.sep) expected = expected.replace("/", os.path.sep) assert util.truncate_path(path) == expected - _p = pytest.param - @pytest.mark.parametrize( "replacements, expected_path, expected_truncated", [ # [ repl before truncation, repl after truncation ] From a4dabb66cf0672b2756811cb20d1e07a1f452f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 11 May 2025 12:09:26 +0100 Subject: [PATCH 031/728] Fix extension arg type in the legalize path call Also tighten `filetype` attribute type - empty value was previously handled to accommodate a couple of tests, but they aren't failing anymore, it seems. --- beets/library.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/beets/library.py b/beets/library.py index 1fe253c18..8fd1c8022 100644 --- a/beets/library.py +++ b/beets/library.py @@ -349,6 +349,7 @@ class LibModel(dbcore.Model["Library"]): # Config key that specifies how an instance should be formatted. _format_config_key: str + path: bytes @cached_classproperty def writable_media_fields(cls) -> set[str]: @@ -644,7 +645,7 @@ class Item(LibModel): _format_config_key = "format_item" # Cached album object. Read-only. - __album = None + __album: Album | None = None @cached_classproperty def _relation(cls) -> type[Album]: @@ -663,9 +664,9 @@ class Item(LibModel): ) @property - def filepath(self) -> Path | None: + def filepath(self) -> Path: """The path to the item's file as pathlib.Path.""" - return Path(os.fsdecode(self.path)) if self.path else self.path + return Path(os.fsdecode(self.path)) @property def _cached_album(self): @@ -1126,7 +1127,7 @@ class Item(LibModel): ) lib_path_str, fallback = util.legalize_path( - subpath, db.replacements, os.path.splitext(self.path)[1] + subpath, db.replacements, self.filepath.suffix ) if fallback: # Print an error message if legalization fell back to From 996a116a6260ab0ff660f2ad1f2fb234b3c0d409 Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Sun, 22 Jan 2023 11:48:52 +1000 Subject: [PATCH 032/728] artresizer: type module --- beets/util/artresizer.py | 123 ++++++++++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 34 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index ffbc2edba..18a5ef82d 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -22,6 +22,8 @@ import platform import re import subprocess from itertools import chain +from typing import AnyStr, Tuple, Optional, Mapping, Union +from PIL import Image from urllib.parse import urlencode from beets import logging, util @@ -32,7 +34,7 @@ PROXY_URL = "https://images.weserv.nl/" log = logging.getLogger("beets") -def resize_url(url, maxwidth, quality=0): +def resize_url(url: str, maxwidth: int, quality: int = 0) -> str: """Return a proxied image URL that resizes the original image to maxwidth (preserving aspect ratio). """ @@ -124,8 +126,13 @@ class IMBackend(LocalBackend): self.compare_cmd = ["magick", "compare"] def resize( - self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 - ): + self, + maxwidth: int, + path_in: AnyStr, + path_out: Optional[AnyStr] = None, + quality: int = 0, + max_filesize: int = 0, + ) -> AnyStr: """Resize using ImageMagick. Use the ``magick`` program or ``convert`` on older versions. Return @@ -174,7 +181,7 @@ class IMBackend(LocalBackend): return path_out - def get_size(self, path_in): + def get_size(self, path_in: str) -> Optional[Tuple[int, ...]]: cmd = self.identify_cmd + [ "-format", "%w %h", @@ -199,7 +206,7 @@ class IMBackend(LocalBackend): log.warning("Could not understand IM output: {0!r}", out) return None - def deinterlace(self, path_in, path_out=None): + def deinterlace(self, path_in: AnyStr, path_out: Optional[AnyStr] = None) -> AnyStr: if not path_out: path_out = get_temp_filename(__name__, "deinterlace_IM_", path_in) @@ -217,7 +224,7 @@ class IMBackend(LocalBackend): # FIXME: Should probably issue a warning? return path_in - def get_format(self, filepath): + def get_format(self, filepath: AnyStr) -> Optional[bytes]: cmd = self.identify_cmd + ["-format", "%[magick]", syspath(filepath)] try: @@ -226,7 +233,12 @@ class IMBackend(LocalBackend): # FIXME: Should probably issue a warning? return None - def convert_format(self, source, target, deinterlaced): + def convert_format( + self, + source: AnyStr, + target: AnyStr, + deinterlaced: bool, + ) -> AnyStr: cmd = self.convert_cmd + [ syspath(source), *(["-interlace", "none"] if deinterlaced else []), @@ -243,10 +255,15 @@ class IMBackend(LocalBackend): return source @property - def can_compare(self): + def can_compare(self) -> bool: return self.version() > (6, 8, 7) - def compare(self, im1, im2, compare_threshold): + def compare( + self, + im1: Image, + im2: Image, + compare_threshold: float, + ) -> Optional[bool]: is_windows = platform.system() == "Windows" # Converting images to grayscale tends to minimize the weight @@ -329,10 +346,10 @@ class IMBackend(LocalBackend): return phash_diff <= compare_threshold @property - def can_write_metadata(self): + def can_write_metadata(self) -> bool: return True - def write_metadata(self, file, metadata): + def write_metadata(self, file: AnyStr, metadata: Mapping): assignments = list( chain.from_iterable(("-set", k, v) for k, v in metadata.items()) ) @@ -359,8 +376,13 @@ class PILBackend(LocalBackend): self.version() def resize( - self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 - ): + self, + maxwidth: int, + path_in: AnyStr, + path_out: Optional[AnyStr] = None, + quality: int = 0, + max_filesize: int = 0, + ) -> AnyStr: """Resize using Python Imaging Library (PIL). Return the output path of resized image. """ @@ -429,7 +451,7 @@ class PILBackend(LocalBackend): ) return path_in - def get_size(self, path_in): + def get_size(self, path_in: AnyStr) -> Optional[Tuple[int, int]]: from PIL import Image try: @@ -441,7 +463,11 @@ class PILBackend(LocalBackend): ) return None - def deinterlace(self, path_in, path_out=None): + def deinterlace( + self, + path_in: AnyStr, + path_out: Optional[AnyStr] = None, + ) -> AnyStr: if not path_out: path_out = get_temp_filename(__name__, "deinterlace_PIL_", path_in) @@ -455,7 +481,7 @@ class PILBackend(LocalBackend): # FIXME: Should probably issue a warning? return path_in - def get_format(self, filepath): + def get_format(self, filepath: AnyStr) -> Optional[str]: from PIL import Image, UnidentifiedImageError try: @@ -470,7 +496,12 @@ class PILBackend(LocalBackend): log.exception("failed to detect image format for {}", filepath) return None - def convert_format(self, source, target, deinterlaced): + def convert_format( + self, + source: AnyStr, + target: AnyStr, + deinterlaced: bool, + ) -> str: from PIL import Image, UnidentifiedImageError try: @@ -488,18 +519,23 @@ class PILBackend(LocalBackend): return source @property - def can_compare(self): + def can_compare(self) -> bool: return False - def compare(self, im1, im2, compare_threshold): + def compare( + self, + im1: Image, + im2: Image, + compare_threshold: float, + ): # It is an error to call this when ArtResizer.can_compare is not True. raise NotImplementedError() @property - def can_write_metadata(self): + def can_write_metadata(self) -> bool: return True - def write_metadata(self, file, metadata): + def write_metadata(self, file: AnyStr, metadata: Mapping): from PIL import Image, PngImagePlugin # FIXME: Detect and handle other file types (currently, the only user @@ -523,7 +559,7 @@ class Shareable(type): cls._instance = None @property - def shared(cls): + def shared(cls) -> 'Shareable': if cls._instance is None: cls._instance = cls() return cls._instance @@ -554,14 +590,19 @@ class ArtResizer(metaclass=Shareable): self.local_method = None @property - def method(self): + def method(self) -> str: if self.local: return self.local_method.NAME else: return "WEBPROXY" def resize( - self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 + self, + maxwidth: int, + path_in: AnyStr, + path_out: Optional[AnyStr]=None, + quality: int = 0, + max_filesize: int = 0, ): """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a @@ -580,7 +621,11 @@ class ArtResizer(metaclass=Shareable): # Handled by `proxy_url` already. return path_in - def deinterlace(self, path_in, path_out=None): + def deinterlace( + self, + path_in: AnyStr, + path_out: Optional[AnyStr] = None, + ) -> AnyStr: """Deinterlace an image. Only available locally. @@ -591,7 +636,7 @@ class ArtResizer(metaclass=Shareable): # FIXME: Should probably issue a warning? return path_in - def proxy_url(self, maxwidth, url, quality=0): + def proxy_url(self, maxwidth: int, url: str, quality: int = 0): """Modifies an image URL according the method, returning a new URL. For WEBPROXY, a URL on the proxy server is returned. Otherwise, the URL is returned unmodified. @@ -603,13 +648,13 @@ class ArtResizer(metaclass=Shareable): return resize_url(url, maxwidth, quality) @property - def local(self): + def local(self) -> bool: """A boolean indicating whether the resizing method is performed locally (i.e., PIL or ImageMagick). """ return self.local_method is not None - def get_size(self, path_in): + def get_size(self, path_in: AnyStr) -> Union[Tuple[int, int], AnyStr]: """Return the size of an image file as an int couple (width, height) in pixels. @@ -621,7 +666,7 @@ class ArtResizer(metaclass=Shareable): # FIXME: Should probably issue a warning? return path_in - def get_format(self, path_in): + def get_format(self, path_in: AnyStr) -> Optional[str]: """Returns the format of the image as a string. Only available locally. @@ -632,7 +677,12 @@ class ArtResizer(metaclass=Shareable): # FIXME: Should probably issue a warning? return None - def reformat(self, path_in, new_format, deinterlaced=True): + def reformat( + self, + path_in: AnyStr, + new_format: str, + deinterlaced: bool = True, + ) -> AnyStr: """Converts image to desired format, updating its extension, but keeping the same filename. @@ -664,7 +714,7 @@ class ArtResizer(metaclass=Shareable): return result_path @property - def can_compare(self): + def can_compare(self) -> bool: """A boolean indicating whether image comparison is available""" if self.local: @@ -672,7 +722,12 @@ class ArtResizer(metaclass=Shareable): else: return False - def compare(self, im1, im2, compare_threshold): + def compare( + self, + im1: Image, + im2: Image, + compare_threshold: float, + ) -> Optional[bool]: """Return a boolean indicating whether two images are similar. Only available locally. @@ -684,7 +739,7 @@ class ArtResizer(metaclass=Shareable): return None @property - def can_write_metadata(self): + def can_write_metadata(self) -> bool: """A boolean indicating whether writing image metadata is supported.""" if self.local: @@ -692,7 +747,7 @@ class ArtResizer(metaclass=Shareable): else: return False - def write_metadata(self, file, metadata): + def write_metadata(self, file: AnyStr, metadata: Mapping): """Write key-value metadata to the image file. Only available locally. Currently, expects the image to be a PNG file. From 7acfe8932a9dc6af34979a06722af395a9b183bf Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Sun, 22 Jan 2023 12:12:44 +1000 Subject: [PATCH 033/728] artresizer: add some missing typings --- beets/util/artresizer.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 18a5ef82d..24c8e5e68 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -58,7 +58,7 @@ _NOT_AVAILABLE = object() class LocalBackend: @classmethod - def available(cls): + def available(cls) -> bool: try: cls.version() return True @@ -76,7 +76,7 @@ class IMBackend(LocalBackend): _legacy = None @classmethod - def version(cls): + def version(cls) -> Optional[Union[object, Tuple[int, int, int]]]: """Obtain and cache ImageMagick version. Raises `LocalBackendNotAvailableError` if not available. @@ -206,7 +206,11 @@ class IMBackend(LocalBackend): log.warning("Could not understand IM output: {0!r}", out) return None - def deinterlace(self, path_in: AnyStr, path_out: Optional[AnyStr] = None) -> AnyStr: + def deinterlace( + self, + path_in: AnyStr, + path_out: Optional[AnyStr] = None, + ) -> AnyStr: if not path_out: path_out = get_temp_filename(__name__, "deinterlace_IM_", path_in) @@ -603,7 +607,7 @@ class ArtResizer(metaclass=Shareable): path_out: Optional[AnyStr]=None, quality: int = 0, max_filesize: int = 0, - ): + ) -> AnyStr: """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a temporary file and encodes with the specified quality level. @@ -636,7 +640,7 @@ class ArtResizer(metaclass=Shareable): # FIXME: Should probably issue a warning? return path_in - def proxy_url(self, maxwidth: int, url: str, quality: int = 0): + def proxy_url(self, maxwidth: int, url: str, quality: int = 0) -> str: """Modifies an image URL according the method, returning a new URL. For WEBPROXY, a URL on the proxy server is returned. Otherwise, the URL is returned unmodified. From c90ff273153d92364db52f17a62f45319e75fbff Mon Sep 17 00:00:00 2001 From: Serene-Arc <serenical@gmail.com> Date: Mon, 13 Feb 2023 13:20:09 +1000 Subject: [PATCH 034/728] artresizer: make import conditional --- beets/util/artresizer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 24c8e5e68..0eff9b77f 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -22,13 +22,15 @@ import platform import re import subprocess from itertools import chain -from typing import AnyStr, Tuple, Optional, Mapping, Union -from PIL import Image +from typing import AnyStr, Tuple, Optional, Mapping, Union, TYPE_CHECKING from urllib.parse import urlencode from beets import logging, util from beets.util import displayable_path, get_temp_filename, syspath +if TYPE_CHECKING: + from PIL import Image + PROXY_URL = "https://images.weserv.nl/" log = logging.getLogger("beets") From b18e6e0654607ad0f60f8934b49859b3f8c12932 Mon Sep 17 00:00:00 2001 From: wisp3rwind <17089248+wisp3rwind@users.noreply.github.com> Date: Tue, 15 Apr 2025 22:25:13 +0200 Subject: [PATCH 035/728] artresizer: revise typings This is more of a "what should the types be", in particular regarding paths, it has not yet been run through mypy. That will be done next, which is probably going to highlight a bunch of issues that should lead to either the code being fixed, or the types adjusted. --- beets/util/artresizer.py | 160 ++++++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 71 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 0eff9b77f..1baca3cd0 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -16,20 +16,26 @@ public resizing proxy if neither is available. """ +from __future__ import annotations + import os import os.path import platform import re import subprocess from itertools import chain -from typing import AnyStr, Tuple, Optional, Mapping, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar, Mapping, Self from urllib.parse import urlencode from beets import logging, util from beets.util import displayable_path, get_temp_filename, syspath if TYPE_CHECKING: - from PIL import Image + try: + from PIL import Image + except ImportError: + from typing import Any + Image = Any PROXY_URL = "https://images.weserv.nl/" @@ -58,7 +64,10 @@ class LocalBackendNotAvailableError(Exception): _NOT_AVAILABLE = object() +# FIXME: Turn this into an ABC with all methods that a backend should have class LocalBackend: + NAME: ClassVar[str] + @classmethod def available(cls) -> bool: try: @@ -74,11 +83,11 @@ class IMBackend(LocalBackend): # These fields are used as a cache for `version()`. `_legacy` indicates # whether the modern `magick` binary is available or whether to fall back # to the old-style `convert`, `identify`, etc. commands. - _version = None - _legacy = None + _version: tuple[int, int, int] | None = None + _legacy: bool | None = None @classmethod - def version(cls) -> Optional[Union[object, Tuple[int, int, int]]]: + def version(cls) -> tuple[int, int, int]: """Obtain and cache ImageMagick version. Raises `LocalBackendNotAvailableError` if not available. @@ -107,7 +116,7 @@ class IMBackend(LocalBackend): else: return cls._version - def __init__(self): + def __init__(self) -> None: """Initialize a wrapper around ImageMagick for local image operations. Stores the ImageMagick version and legacy flag. If ImageMagick is not @@ -130,11 +139,11 @@ class IMBackend(LocalBackend): def resize( self, maxwidth: int, - path_in: AnyStr, - path_out: Optional[AnyStr] = None, + path_in: bytes, + path_out: bytes | None = None, quality: int = 0, max_filesize: int = 0, - ) -> AnyStr: + ) -> bytes: """Resize using ImageMagick. Use the ``magick`` program or ``convert`` on older versions. Return @@ -183,7 +192,7 @@ class IMBackend(LocalBackend): return path_out - def get_size(self, path_in: str) -> Optional[Tuple[int, ...]]: + def get_size(self, path_in: bytes) -> tuple[int, int] | None: cmd = self.identify_cmd + [ "-format", "%w %h", @@ -209,10 +218,10 @@ class IMBackend(LocalBackend): return None def deinterlace( - self, - path_in: AnyStr, - path_out: Optional[AnyStr] = None, - ) -> AnyStr: + self, + path_in: bytes, + path_out: bytes | None = None, + ) -> bytes: if not path_out: path_out = get_temp_filename(__name__, "deinterlace_IM_", path_in) @@ -230,8 +239,8 @@ class IMBackend(LocalBackend): # FIXME: Should probably issue a warning? return path_in - def get_format(self, filepath: AnyStr) -> Optional[bytes]: - cmd = self.identify_cmd + ["-format", "%[magick]", syspath(filepath)] + def get_format(self, path_in: bytes) -> bytes | None: + cmd = self.identify_cmd + ["-format", "%[magick]", syspath(path_in)] try: return util.command_output(cmd).stdout @@ -241,10 +250,10 @@ class IMBackend(LocalBackend): def convert_format( self, - source: AnyStr, - target: AnyStr, + source: bytes, + target: bytes, deinterlaced: bool, - ) -> AnyStr: + ) -> bytes: cmd = self.convert_cmd + [ syspath(source), *(["-interlace", "none"] if deinterlaced else []), @@ -265,11 +274,11 @@ class IMBackend(LocalBackend): return self.version() > (6, 8, 7) def compare( - self, - im1: Image, - im2: Image, - compare_threshold: float, - ) -> Optional[bool]: + self, + im1: bytes, + im2: bytes, + compare_threshold: float, + ) -> bool | None: is_windows = platform.system() == "Windows" # Converting images to grayscale tends to minimize the weight @@ -355,7 +364,7 @@ class IMBackend(LocalBackend): def can_write_metadata(self) -> bool: return True - def write_metadata(self, file: AnyStr, metadata: Mapping): + def write_metadata(self, file: bytes, metadata: Mapping) -> None: assignments = list( chain.from_iterable(("-set", k, v) for k, v in metadata.items()) ) @@ -368,13 +377,13 @@ class PILBackend(LocalBackend): NAME = "PIL" @classmethod - def version(cls): + def version(cls) -> None: try: __import__("PIL", fromlist=["Image"]) except ImportError: raise LocalBackendNotAvailableError() - def __init__(self): + def __init__(self) -> None: """Initialize a wrapper around PIL for local image operations. If PIL is not available, raise an Exception. @@ -384,11 +393,11 @@ class PILBackend(LocalBackend): def resize( self, maxwidth: int, - path_in: AnyStr, - path_out: Optional[AnyStr] = None, + path_in: bytes, + path_out: bytes | None = None, quality: int = 0, max_filesize: int = 0, - ) -> AnyStr: + ) -> bytes: """Resize using Python Imaging Library (PIL). Return the output path of resized image. """ @@ -457,7 +466,7 @@ class PILBackend(LocalBackend): ) return path_in - def get_size(self, path_in: AnyStr) -> Optional[Tuple[int, int]]: + def get_size(self, path_in: bytes) -> tuple[int, int] | None: from PIL import Image try: @@ -470,10 +479,10 @@ class PILBackend(LocalBackend): return None def deinterlace( - self, - path_in: AnyStr, - path_out: Optional[AnyStr] = None, - ) -> AnyStr: + self, + path_in: bytes, + path_out: bytes | None = None, + ) -> bytes: if not path_out: path_out = get_temp_filename(__name__, "deinterlace_PIL_", path_in) @@ -487,11 +496,11 @@ class PILBackend(LocalBackend): # FIXME: Should probably issue a warning? return path_in - def get_format(self, filepath: AnyStr) -> Optional[str]: + def get_format(self, path_in: bytes) -> bytes | None: from PIL import Image, UnidentifiedImageError try: - with Image.open(syspath(filepath)) as im: + with Image.open(syspath(path_in)) as im: return im.format except ( ValueError, @@ -499,15 +508,15 @@ class PILBackend(LocalBackend): UnidentifiedImageError, FileNotFoundError, ): - log.exception("failed to detect image format for {}", filepath) + log.exception("failed to detect image format for {}", path_in) return None def convert_format( self, - source: AnyStr, - target: AnyStr, + source: bytes, + target: bytes, deinterlaced: bool, - ) -> str: + ) -> bytes: from PIL import Image, UnidentifiedImageError try: @@ -529,11 +538,11 @@ class PILBackend(LocalBackend): return False def compare( - self, - im1: Image, - im2: Image, - compare_threshold: float, - ): + self, + im1: bytes, + im2: bytes, + compare_threshold: float, + ) -> bool | None: # It is an error to call this when ArtResizer.can_compare is not True. raise NotImplementedError() @@ -541,7 +550,7 @@ class PILBackend(LocalBackend): def can_write_metadata(self) -> bool: return True - def write_metadata(self, file: AnyStr, metadata: Mapping): + def write_metadata(self, file: bytes, metadata: Mapping) -> None: from PIL import Image, PngImagePlugin # FIXME: Detect and handle other file types (currently, the only user @@ -560,18 +569,18 @@ class Shareable(type): ``MyClass()`` to construct a new object works as usual. """ - def __init__(cls, name, bases, dict): + def __init__(cls, name, bases, dict) -> None: super().__init__(name, bases, dict) cls._instance = None @property - def shared(cls) -> 'Shareable': + def shared(cls) -> Self: if cls._instance is None: cls._instance = cls() return cls._instance -BACKEND_CLASSES = [ +BACKEND_CLASSES: list[LocalBackend] = [ IMBackend, PILBackend, ] @@ -580,7 +589,7 @@ BACKEND_CLASSES = [ class ArtResizer(metaclass=Shareable): """A singleton class that performs image resizes.""" - def __init__(self): + def __init__(self) -> None: """Create a resizer object with an inferred method.""" # Check if a local backend is available, and store an instance of the # backend class. Otherwise, fallback to the web proxy. @@ -592,6 +601,15 @@ class ArtResizer(metaclass=Shareable): except LocalBackendNotAvailableError: continue else: + # FIXME: Turn WEBPROXY into a backend class as well to remove all + # the special casing. Then simply delegate all methods to the + # backends. (How does proxy_url fit in here, however?) + # Use an ABC (or maybe a typing Protocol?) for backend + # methods, such that both individual backends as well as + # ArtResizer implement it. + # It should probably be configurable which backends classes to + # consider, similar to fetchart or lyrics backends (i.e. a list + # of backends sorted by priority). log.debug("artresizer: method is WEBPROXY") self.local_method = None @@ -605,11 +623,11 @@ class ArtResizer(metaclass=Shareable): def resize( self, maxwidth: int, - path_in: AnyStr, - path_out: Optional[AnyStr]=None, + path_in: bytes, + path_out: bytes | None = None, quality: int = 0, max_filesize: int = 0, - ) -> AnyStr: + ) -> bytes: """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a temporary file and encodes with the specified quality level. @@ -628,10 +646,10 @@ class ArtResizer(metaclass=Shareable): return path_in def deinterlace( - self, - path_in: AnyStr, - path_out: Optional[AnyStr] = None, - ) -> AnyStr: + self, + path_in: bytes, + path_out: bytes | None = None, + ) -> bytes: """Deinterlace an image. Only available locally. @@ -660,7 +678,7 @@ class ArtResizer(metaclass=Shareable): """ return self.local_method is not None - def get_size(self, path_in: AnyStr) -> Union[Tuple[int, int], AnyStr]: + def get_size(self, path_in: bytes) -> tuple[int, int] | None: """Return the size of an image file as an int couple (width, height) in pixels. @@ -672,7 +690,7 @@ class ArtResizer(metaclass=Shareable): # FIXME: Should probably issue a warning? return path_in - def get_format(self, path_in: AnyStr) -> Optional[str]: + def get_format(self, path_in: bytes) -> bytes | None: """Returns the format of the image as a string. Only available locally. @@ -684,11 +702,11 @@ class ArtResizer(metaclass=Shareable): return None def reformat( - self, - path_in: AnyStr, - new_format: str, - deinterlaced: bool = True, - ) -> AnyStr: + self, + path_in: bytes, + new_format: str, + deinterlaced: bool = True, + ) -> bytes: """Converts image to desired format, updating its extension, but keeping the same filename. @@ -729,11 +747,11 @@ class ArtResizer(metaclass=Shareable): return False def compare( - self, - im1: Image, - im2: Image, - compare_threshold: float, - ) -> Optional[bool]: + self, + im1: bytes, + im2: bytes, + compare_threshold: float, + ) -> bool | None: """Return a boolean indicating whether two images are similar. Only available locally. @@ -753,7 +771,7 @@ class ArtResizer(metaclass=Shareable): else: return False - def write_metadata(self, file: AnyStr, metadata: Mapping): + def write_metadata(self, file: bytes, metadata: Mapping) -> None: """Write key-value metadata to the image file. Only available locally. Currently, expects the image to be a PNG file. From 720023c76ffb18f7f1c37cbc59c9bb0f121d1579 Mon Sep 17 00:00:00 2001 From: wisp3rwind <17089248+wisp3rwind@users.noreply.github.com> Date: Tue, 15 Apr 2025 23:46:17 +0200 Subject: [PATCH 036/728] artresizer: adjust code & typings to satisfy mypy Notably, this replaces the `Shareable` metaclass by a different implementation of a descriptor: We don't really need to modify class creation here, because the singleton is only available via the `shared` method, not via the constructor. Additionally, it appears that mypy can understand the new code. --- beets/util/__init__.py | 25 +++++ beets/util/artresizer.py | 218 ++++++++++++++++++++++++++------------- 2 files changed, 170 insertions(+), 73 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index e17de1f51..78b1fafc9 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -40,6 +40,7 @@ from typing import ( Any, AnyStr, Callable, + Generic, Iterable, NamedTuple, TypeVar, @@ -1041,6 +1042,30 @@ class cached_classproperty: return self.cache[owner] +T = TypeVar("T") + + +class LazySharedInstance(Generic[T]): + """A descriptor that provides access to a lazily-created shared instance of + the containing class, while calling the class constructor to construct a + new object works as usual. + """ + + _instance: T | None = None + + def __get__(self, instance: T | None, owner: type[T]) -> T: + if instance is not None: + raise RuntimeError( + "shared instances must be obtained from the class property, " + "not an instance" + ) + + if self._instance is None: + self._instance = owner() + + return self._instance + + def get_module_tempdir(module: str) -> Path: """Return the temporary directory for the given module. diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 1baca3cd0..55ea1754e 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -23,19 +23,19 @@ import os.path import platform import re import subprocess +from abc import ABC, abstractmethod +from enum import Enum from itertools import chain -from typing import TYPE_CHECKING, ClassVar, Mapping, Self +from typing import Any, ClassVar, Mapping from urllib.parse import urlencode from beets import logging, util -from beets.util import displayable_path, get_temp_filename, syspath - -if TYPE_CHECKING: - try: - from PIL import Image - except ImportError: - from typing import Any - Image = Any +from beets.util import ( + LazySharedInstance, + displayable_path, + get_temp_filename, + syspath, +) PROXY_URL = "https://images.weserv.nl/" @@ -61,13 +61,23 @@ class LocalBackendNotAvailableError(Exception): pass -_NOT_AVAILABLE = object() +# Singleton pattern that the typechecker understands: +# https://peps.python.org/pep-0484/#support-for-singleton-types-in-unions +class NotAvailable(Enum): + token = 0 -# FIXME: Turn this into an ABC with all methods that a backend should have -class LocalBackend: +_NOT_AVAILABLE = NotAvailable.token + + +class LocalBackend(ABC): NAME: ClassVar[str] + @classmethod + @abstractmethod + def version(cls) -> Any: + pass + @classmethod def available(cls) -> bool: try: @@ -76,6 +86,63 @@ class LocalBackend: except LocalBackendNotAvailableError: return False + @abstractmethod + def resize( + self, + maxwidth: int, + path_in: bytes, + path_out: bytes | None = None, + quality: int = 0, + max_filesize: int = 0, + ) -> bytes: + pass + + @abstractmethod + def get_size(self, path_in: bytes) -> tuple[int, int] | None: + pass + + @abstractmethod + def deinterlace( + self, + path_in: bytes, + path_out: bytes | None = None, + ) -> bytes: + pass + + @abstractmethod + def get_format(self, path_in: bytes) -> str | None: + pass + + @abstractmethod + def convert_format( + self, + source: bytes, + target: bytes, + deinterlaced: bool, + ) -> bytes: + pass + + @property + def can_compare(self) -> bool: + return False + + def compare( + self, + im1: bytes, + im2: bytes, + compare_threshold: float, + ) -> bool | None: + # It is an error to call this when ArtResizer.can_compare is not True. + raise NotImplementedError() + + @property + def can_write_metadata(self) -> bool: + return False + + def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None: + # It is an error to call this when ArtResizer.can_write_metadata is not True. + raise NotImplementedError() + class IMBackend(LocalBackend): NAME = "ImageMagick" @@ -83,7 +150,7 @@ class IMBackend(LocalBackend): # These fields are used as a cache for `version()`. `_legacy` indicates # whether the modern `magick` binary is available or whether to fall back # to the old-style `convert`, `identify`, etc. commands. - _version: tuple[int, int, int] | None = None + _version: tuple[int, int, int] | NotAvailable | None = None _legacy: bool | None = None @classmethod @@ -111,11 +178,16 @@ class IMBackend(LocalBackend): ) cls._legacy = legacy - if cls._version is _NOT_AVAILABLE: + # cls._version is never None here, but mypy doesn't get that + if cls._version is _NOT_AVAILABLE or cls._version is None: raise LocalBackendNotAvailableError() else: return cls._version + convert_cmd: list[str | bytes] + identify_cmd: list[str | bytes] + compare_cmd: list[str | bytes] + def __init__(self) -> None: """Initialize a wrapper around ImageMagick for local image operations. @@ -163,7 +235,7 @@ class IMBackend(LocalBackend): # with regards to the height. # ImageMagick already seems to default to no interlace, but we include # it here for the sake of explicitness. - cmd = self.convert_cmd + [ + cmd: list[str | bytes] = self.convert_cmd + [ syspath(path_in, prefix=False), "-resize", f"{maxwidth}x>", @@ -193,7 +265,7 @@ class IMBackend(LocalBackend): return path_out def get_size(self, path_in: bytes) -> tuple[int, int] | None: - cmd = self.identify_cmd + [ + cmd: list[str | bytes] = self.identify_cmd + [ "-format", "%w %h", syspath(path_in, prefix=False), @@ -212,11 +284,17 @@ class IMBackend(LocalBackend): ) return None try: - return tuple(map(int, out.split(b" "))) + size = tuple(map(int, out.split(b" "))) except IndexError: log.warning("Could not understand IM output: {0!r}", out) return None + if len(size) != 2: + log.warning("Could not understand IM output: {0!r}", out) + return None + + return size + def deinterlace( self, path_in: bytes, @@ -239,20 +317,23 @@ class IMBackend(LocalBackend): # FIXME: Should probably issue a warning? return path_in - def get_format(self, path_in: bytes) -> bytes | None: + def get_format(self, path_in: bytes) -> str | None: cmd = self.identify_cmd + ["-format", "%[magick]", syspath(path_in)] try: - return util.command_output(cmd).stdout - except subprocess.CalledProcessError: + # Image formats should really only be ASCII strings such as "PNG", + # if anything else is returned, something is off and we return + # None for safety. + return util.command_output(cmd).stdout.decode("ascii", "strict") + except (subprocess.CalledProcessError, UnicodeError): # FIXME: Should probably issue a warning? return None def convert_format( - self, - source: bytes, - target: bytes, - deinterlaced: bool, + self, + source: bytes, + target: bytes, + deinterlaced: bool, ) -> bytes: cmd = self.convert_cmd + [ syspath(source), @@ -318,6 +399,10 @@ class IMBackend(LocalBackend): close_fds=not is_windows, ) + # help out mypy + assert convert_proc.stdout is not None + assert convert_proc.stderr is not None + # Check the convert output. We're not interested in the # standard output; that gets piped to the next stage. convert_proc.stdout.close() @@ -364,7 +449,7 @@ class IMBackend(LocalBackend): def can_write_metadata(self) -> bool: return True - def write_metadata(self, file: bytes, metadata: Mapping) -> None: + def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None: assignments = list( chain.from_iterable(("-set", k, v) for k, v in metadata.items()) ) @@ -391,12 +476,12 @@ class PILBackend(LocalBackend): self.version() def resize( - self, - maxwidth: int, - path_in: bytes, - path_out: bytes | None = None, - quality: int = 0, - max_filesize: int = 0, + self, + maxwidth: int, + path_in: bytes, + path_out: bytes | None = None, + quality: int = 0, + max_filesize: int = 0, ) -> bytes: """Resize using Python Imaging Library (PIL). Return the output path of resized image. @@ -496,7 +581,7 @@ class PILBackend(LocalBackend): # FIXME: Should probably issue a warning? return path_in - def get_format(self, path_in: bytes) -> bytes | None: + def get_format(self, path_in: bytes) -> str | None: from PIL import Image, UnidentifiedImageError try: @@ -512,10 +597,10 @@ class PILBackend(LocalBackend): return None def convert_format( - self, - source: bytes, - target: bytes, - deinterlaced: bool, + self, + source: bytes, + target: bytes, + deinterlaced: bool, ) -> bytes: from PIL import Image, UnidentifiedImageError @@ -550,7 +635,7 @@ class PILBackend(LocalBackend): def can_write_metadata(self) -> bool: return True - def write_metadata(self, file: bytes, metadata: Mapping) -> None: + def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None: from PIL import Image, PngImagePlugin # FIXME: Detect and handle other file types (currently, the only user @@ -558,36 +643,20 @@ class PILBackend(LocalBackend): im = Image.open(syspath(file)) meta = PngImagePlugin.PngInfo() for k, v in metadata.items(): - meta.add_text(k, v, 0) + meta.add_text(k, v, zip=False) im.save(os.fsdecode(file), "PNG", pnginfo=meta) -class Shareable(type): - """A pseudo-singleton metaclass that allows both shared and - non-shared instances. The ``MyClass.shared`` property holds a - lazily-created shared instance of ``MyClass`` while calling - ``MyClass()`` to construct a new object works as usual. - """ - - def __init__(cls, name, bases, dict) -> None: - super().__init__(name, bases, dict) - cls._instance = None - - @property - def shared(cls) -> Self: - if cls._instance is None: - cls._instance = cls() - return cls._instance - - -BACKEND_CLASSES: list[LocalBackend] = [ +BACKEND_CLASSES: list[type[LocalBackend]] = [ IMBackend, PILBackend, ] -class ArtResizer(metaclass=Shareable): - """A singleton class that performs image resizes.""" +class ArtResizer: + """A class that dispatches image operations to an available backend.""" + + local_method: LocalBackend | None def __init__(self) -> None: """Create a resizer object with an inferred method.""" @@ -613,9 +682,11 @@ class ArtResizer(metaclass=Shareable): log.debug("artresizer: method is WEBPROXY") self.local_method = None + shared: LazySharedInstance[ArtResizer] = LazySharedInstance() + @property def method(self) -> str: - if self.local: + if self.local_method is not None: return self.local_method.NAME else: return "WEBPROXY" @@ -633,7 +704,7 @@ class ArtResizer(metaclass=Shareable): temporary file and encodes with the specified quality level. For WEBPROXY, returns `path_in` unmodified. """ - if self.local: + if self.local_method is not None: return self.local_method.resize( maxwidth, path_in, @@ -654,7 +725,7 @@ class ArtResizer(metaclass=Shareable): Only available locally. """ - if self.local: + if self.local_method is not None: return self.local_method.deinterlace(path_in, path_out) else: # FIXME: Should probably issue a warning? @@ -684,18 +755,19 @@ class ArtResizer(metaclass=Shareable): Only available locally. """ - if self.local: + if self.local_method is not None: return self.local_method.get_size(path_in) else: - # FIXME: Should probably issue a warning? - return path_in + raise RuntimeError( + "image cannot be obtained without artresizer backend" + ) - def get_format(self, path_in: bytes) -> bytes | None: + def get_format(self, path_in: bytes) -> str | None: """Returns the format of the image as a string. Only available locally. """ - if self.local: + if self.local_method is not None: return self.local_method.get_format(path_in) else: # FIXME: Should probably issue a warning? @@ -712,7 +784,7 @@ class ArtResizer(metaclass=Shareable): Only available locally. """ - if not self.local: + if self.local_method is None: # FIXME: Should probably issue a warning? return path_in @@ -741,7 +813,7 @@ class ArtResizer(metaclass=Shareable): def can_compare(self) -> bool: """A boolean indicating whether image comparison is available""" - if self.local: + if self.local_method is not None: return self.local_method.can_compare else: return False @@ -756,7 +828,7 @@ class ArtResizer(metaclass=Shareable): Only available locally. """ - if self.local: + if self.local_method is not None: return self.local_method.compare(im1, im2, compare_threshold) else: # FIXME: Should probably issue a warning? @@ -766,17 +838,17 @@ class ArtResizer(metaclass=Shareable): def can_write_metadata(self) -> bool: """A boolean indicating whether writing image metadata is supported.""" - if self.local: + if self.local_method is not None: return self.local_method.can_write_metadata else: return False - def write_metadata(self, file: bytes, metadata: Mapping) -> None: + def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None: """Write key-value metadata to the image file. Only available locally. Currently, expects the image to be a PNG file. """ - if self.local: + if self.local_method is not None: self.local_method.write_metadata(file, metadata) else: # FIXME: Should probably issue a warning? From aa49385d27c85297be270ad12bb9db3e66a10f4b Mon Sep 17 00:00:00 2001 From: wisp3rwind <17089248+wisp3rwind@users.noreply.github.com> Date: Tue, 13 May 2025 10:48:57 +0200 Subject: [PATCH 037/728] artresizer: address review --- beets/util/__init__.py | 3 --- beets/util/artresizer.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 78b1fafc9..964051d85 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -1042,9 +1042,6 @@ class cached_classproperty: return self.cache[owner] -T = TypeVar("T") - - class LazySharedInstance(Generic[T]): """A descriptor that provides access to a lazily-created shared instance of the containing class, while calling the class constructor to construct a diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 55ea1754e..33b98c413 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -76,10 +76,15 @@ class LocalBackend(ABC): @classmethod @abstractmethod def version(cls) -> Any: + """Return the backend version if its dependencies are satisfied or + raise `LocalBackendNotAvailableError`. + """ pass @classmethod def available(cls) -> bool: + """Return `True` this backend's dependencies are satisfied and it can + be used, `False` otherwise.""" try: cls.version() return True @@ -95,10 +100,15 @@ class LocalBackend(ABC): quality: int = 0, max_filesize: int = 0, ) -> bytes: + """Resize an image to the given width and return the output path. + + On error, logs a warning and returns `path_in`. + """ pass @abstractmethod def get_size(self, path_in: bytes) -> tuple[int, int] | None: + """Return the (width, height) of the image or None if unavailable.""" pass @abstractmethod @@ -107,10 +117,15 @@ class LocalBackend(ABC): path_in: bytes, path_out: bytes | None = None, ) -> bytes: + """Remove interlacing from an image and return the output path. + + On error, logs a warning and returns `path_in`. + """ pass @abstractmethod def get_format(self, path_in: bytes) -> str | None: + """Return the image format (e.g., 'PNG') or None if undetectable.""" pass @abstractmethod @@ -120,10 +135,15 @@ class LocalBackend(ABC): target: bytes, deinterlaced: bool, ) -> bytes: + """Convert an image to a new format and return the new file path. + + On error, logs a warning and returns `source`. + """ pass @property def can_compare(self) -> bool: + """Indicate whether image comparison is supported by this backend.""" return False def compare( @@ -132,14 +152,24 @@ class LocalBackend(ABC): im2: bytes, compare_threshold: float, ) -> bool | None: + """Compare two images and return `True` if they are similar enough, or + `None` if there is an error. + + This must only be called if `self.can_compare()` returns `True`. + """ # It is an error to call this when ArtResizer.can_compare is not True. raise NotImplementedError() @property def can_write_metadata(self) -> bool: + """Indicate whether writing metadata to images is supported.""" return False def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None: + """Write key-value metadata into the image file. + + This must only be called if `self.can_write_metadata()` returns `True`. + """ # It is an error to call this when ArtResizer.can_write_metadata is not True. raise NotImplementedError() From 26008eb9229567da03587eeb1d4be8bf409b94c0 Mon Sep 17 00:00:00 2001 From: wisp3rwind <17089248+wisp3rwind@users.noreply.github.com> Date: Tue, 13 May 2025 11:00:50 +0200 Subject: [PATCH 038/728] add example for LazySharedInstance --- beets/util/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 964051d85..02ab9cf56 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -1046,6 +1046,32 @@ class LazySharedInstance(Generic[T]): """A descriptor that provides access to a lazily-created shared instance of the containing class, while calling the class constructor to construct a new object works as usual. + + ``` + ID: int = 0 + + class Foo: + def __init__(): + global ID + + self.id = ID + ID += 1 + + def func(self): + print(self.id) + + shared: LazySharedInstance[Foo] = LazySharedInstance() + + a0 = Foo() + a1 = Foo.shared + a2 = Foo() + a3 = Foo.shared + + a0.func() # 0 + a1.func() # 1 + a2.func() # 2 + a3.func() # 1 + ``` """ _instance: T | None = None From 9147577b2b19f43ca827e9650261a86fb0450cef Mon Sep 17 00:00:00 2001 From: Sebastian Mohr <sebastian@mohrenclan.de> Date: Tue, 13 May 2025 12:59:17 +0200 Subject: [PATCH 039/728] Moved importer into new files --- beets/importer/session.py | 303 ++++++++ beets/importer/stages.py | 396 +++++++++++ beets/importer/state.py | 142 ++++ beets/{importer.py => importer/tasks.py} | 848 ++--------------------- 4 files changed, 903 insertions(+), 786 deletions(-) create mode 100644 beets/importer/session.py create mode 100644 beets/importer/stages.py create mode 100644 beets/importer/state.py rename beets/{importer.py => importer/tasks.py} (60%) diff --git a/beets/importer/session.py b/beets/importer/session.py new file mode 100644 index 000000000..620eb688e --- /dev/null +++ b/beets/importer/session.py @@ -0,0 +1,303 @@ +# This file is part of beets. +# Copyright 2016, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +from __future__ import annotations + +import os +import time +from typing import TYPE_CHECKING, Sequence + +from beets import config, dbcore, library, logging, plugins, util +from beets.importer.tasks import action +from beets.util import displayable_path, normpath, pipeline, syspath + +from .stages import * +from .state import ImportState + +if TYPE_CHECKING: + from beets.util import PathBytes + + from .tasks import ImportTask + + +QUEUE_SIZE = 128 + +# Global logger. +log = logging.getLogger("beets") + + +class ImportAbortError(Exception): + """Raised when the user aborts the tagging operation.""" + + pass + + +class ImportSession: + """Controls an import action. Subclasses should implement methods to + communicate with the user or otherwise make decisions. + """ + + logger: logging.Logger + paths: list[PathBytes] + lib: library.Library + + _is_resuming: dict[bytes, bool] + _merged_items: set[PathBytes] + _merged_dirs: set[PathBytes] + + def __init__( + self, + lib: library.Library, + loghandler: logging.Handler | None, + paths: Sequence[PathBytes] | None, + query: dbcore.Query | None, + ): + """Create a session. + + Parameters + ---------- + lib : library.Library + The library instance to which items will be imported. + loghandler : logging.Handler or None + A logging handler to use for the session's logger. If None, a + NullHandler will be used. + paths : os.PathLike or None + The paths to be imported. + query : dbcore.Query or None + A query to filter items for import. + """ + self.lib = lib + self.logger = self._setup_logging(loghandler) + self.query = query + self._is_resuming = {} + self._merged_items = set() + self._merged_dirs = set() + + # Normalize the paths. + self.paths = list(map(normpath, paths or [])) + + def _setup_logging(self, loghandler: logging.Handler | None): + logger = logging.getLogger(__name__) + logger.propagate = False + if not loghandler: + loghandler = logging.NullHandler() + logger.handlers = [loghandler] + return logger + + def set_config(self, config): + """Set `config` property from global import config and make + implied changes. + """ + # FIXME: Maybe this function should not exist and should instead + # provide "decision wrappers" like "should_resume()", etc. + iconfig = dict(config) + self.config = iconfig + + # Incremental and progress are mutually exclusive. + if iconfig["incremental"]: + iconfig["resume"] = False + + # When based on a query instead of directories, never + # save progress or try to resume. + if self.query is not None: + iconfig["resume"] = False + iconfig["incremental"] = False + + if iconfig["reflink"]: + iconfig["reflink"] = iconfig["reflink"].as_choice( + ["auto", True, False] + ) + + # Copy, move, reflink, link, and hardlink are mutually exclusive. + if iconfig["move"]: + iconfig["copy"] = False + iconfig["link"] = False + iconfig["hardlink"] = False + iconfig["reflink"] = False + elif iconfig["link"]: + iconfig["copy"] = False + iconfig["move"] = False + iconfig["hardlink"] = False + iconfig["reflink"] = False + elif iconfig["hardlink"]: + iconfig["copy"] = False + iconfig["move"] = False + iconfig["link"] = False + iconfig["reflink"] = False + elif iconfig["reflink"]: + iconfig["copy"] = False + iconfig["move"] = False + iconfig["link"] = False + iconfig["hardlink"] = False + + # Only delete when copying. + if not iconfig["copy"]: + iconfig["delete"] = False + + self.want_resume = config["resume"].as_choice([True, False, "ask"]) + + def tag_log(self, status, paths: Sequence[PathBytes]): + """Log a message about a given album to the importer log. The status + should reflect the reason the album couldn't be tagged. + """ + self.logger.info("{0} {1}", status, displayable_path(paths)) + + def log_choice(self, task: ImportTask, duplicate=False): + """Logs the task's current choice if it should be logged. If + ``duplicate``, then this is a secondary choice after a duplicate was + detected and a decision was made. + """ + paths = task.paths + if duplicate: + # Duplicate: log all three choices (skip, keep both, and trump). + if task.should_remove_duplicates: + self.tag_log("duplicate-replace", paths) + elif task.choice_flag in (action.ASIS, action.APPLY): + self.tag_log("duplicate-keep", paths) + elif task.choice_flag is action.SKIP: + self.tag_log("duplicate-skip", paths) + else: + # Non-duplicate: log "skip" and "asis" choices. + if task.choice_flag is action.ASIS: + self.tag_log("asis", paths) + elif task.choice_flag is action.SKIP: + self.tag_log("skip", paths) + + def should_resume(self, path: PathBytes): + raise NotImplementedError + + def choose_match(self, task: ImportTask): + raise NotImplementedError + + def resolve_duplicate(self, task: ImportTask, found_duplicates): + raise NotImplementedError + + def choose_item(self, task: ImportTask): + raise NotImplementedError + + def run(self): + """Run the import task.""" + self.logger.info("import started {0}", time.asctime()) + self.set_config(config["import"]) + + # Set up the pipeline. + if self.query is None: + stages = [read_tasks(self)] + else: + stages = [query_tasks(self)] + + # In pretend mode, just log what would otherwise be imported. + if self.config["pretend"]: + stages += [log_files(self)] + else: + if self.config["group_albums"] and not self.config["singletons"]: + # Split directory tasks into one task for each album. + stages += [group_albums(self)] + + # These stages either talk to the user to get a decision or, + # in the case of a non-autotagged import, just choose to + # import everything as-is. In *both* cases, these stages + # also add the music to the library database, so later + # stages need to read and write data from there. + if self.config["autotag"]: + stages += [lookup_candidates(self), user_query(self)] + else: + stages += [import_asis(self)] + + # Plugin stages. + for stage_func in plugins.early_import_stages(): + stages.append(plugin_stage(self, stage_func)) + for stage_func in plugins.import_stages(): + stages.append(plugin_stage(self, stage_func)) + + stages += [manipulate_files(self)] + + pl = pipeline.Pipeline(stages) + + # Run the pipeline. + plugins.send("import_begin", session=self) + try: + if config["threaded"]: + pl.run_parallel(QUEUE_SIZE) + else: + pl.run_sequential() + except ImportAbortError: + # User aborted operation. Silently stop. + pass + + # Incremental and resumed imports + + def already_imported(self, toppath: PathBytes, paths: Sequence[PathBytes]): + """Returns true if the files belonging to this task have already + been imported in a previous session. + """ + if self.is_resuming(toppath) and all( + [ImportState().progress_has_element(toppath, p) for p in paths] + ): + return True + if self.config["incremental"] and tuple(paths) in self.history_dirs: + return True + + return False + + _history_dirs = None + + @property + def history_dirs(self) -> set[tuple[PathBytes, ...]]: + # FIXME: This could be simplified to a cached property + if self._history_dirs is None: + self._history_dirs = ImportState().taghistory + return self._history_dirs + + def already_merged(self, paths: Sequence[PathBytes]): + """Returns true if all the paths being imported were part of a merge + during previous tasks. + """ + for path in paths: + if path not in self._merged_items and path not in self._merged_dirs: + return False + return True + + def mark_merged(self, paths: Sequence[PathBytes]): + """Mark paths and directories as merged for future reimport tasks.""" + self._merged_items.update(paths) + dirs = { + os.path.dirname(path) if os.path.isfile(syspath(path)) else path + for path in paths + } + self._merged_dirs.update(dirs) + + def is_resuming(self, toppath: PathBytes): + """Return `True` if user wants to resume import of this path. + + You have to call `ask_resume` first to determine the return value. + """ + return self._is_resuming.get(toppath, False) + + def ask_resume(self, toppath: PathBytes): + """If import of `toppath` was aborted in an earlier session, ask + user if they want to resume the import. + + Determines the return value of `is_resuming(toppath)`. + """ + if self.want_resume and ImportState().progress_has(toppath): + # Either accept immediately or prompt for input to decide. + if self.want_resume is True or self.should_resume(toppath): + log.warning( + "Resuming interrupted import of {0}", + util.displayable_path(toppath), + ) + self._is_resuming[toppath] = True + else: + # Clear progress; we're starting from the top. + ImportState().progress_reset(toppath) diff --git a/beets/importer/stages.py b/beets/importer/stages.py new file mode 100644 index 000000000..52b2a221a --- /dev/null +++ b/beets/importer/stages.py @@ -0,0 +1,396 @@ +# This file is part of beets. +# Copyright 2016, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +from __future__ import annotations + +import itertools +import logging +from typing import TYPE_CHECKING, Callable + +from beets import config, plugins +from beets.util import MoveOperation, displayable_path, pipeline + +from .tasks import ( + action, + ImportTask, + ImportTaskFactory, + SentinelImportTask, + SingletonImportTask, +) + +if TYPE_CHECKING: + from beets import library + + from .session import ImportSession + +# Global logger. +log = logging.getLogger("beets") + +# ---------------------------- Producer functions ---------------------------- # +# Functions that are called first i.e. they generate import tasks + + +def read_tasks(session: ImportSession): + """A generator yielding all the albums (as ImportTask objects) found + in the user-specified list of paths. In the case of a singleton + import, yields single-item tasks instead. + """ + skipped = 0 + + for toppath in session.paths: + # Check whether we need to resume the import. + session.ask_resume(toppath) + + # Generate tasks. + task_factory = ImportTaskFactory(toppath, session) + yield from task_factory.tasks() + skipped += task_factory.skipped + + if not task_factory.imported: + log.warning("No files imported from {0}", displayable_path(toppath)) + + # Show skipped directories (due to incremental/resume). + if skipped: + log.info("Skipped {0} paths.", skipped) + + +def query_tasks(session: ImportSession): + """A generator that works as a drop-in-replacement for read_tasks. + Instead of finding files from the filesystem, a query is used to + match items from the library. + """ + if session.config["singletons"]: + # Search for items. + for item in session.lib.items(session.query): + task = SingletonImportTask(None, item) + for task in task.handle_created(session): + yield task + + else: + # Search for albums. + for album in session.lib.albums(session.query): + log.debug( + "yielding album {0}: {1} - {2}", + album.id, + album.albumartist, + album.album, + ) + items = list(album.items()) + _freshen_items(items) + + task = ImportTask(None, [album.item_dir()], items) + for task in task.handle_created(session): + yield task + + +# ---------------------------------- Stages ---------------------------------- # +# Functions that process import tasks, may transform or filter them +# They are chained together in the pipeline e.g. stage2(stage1(task)) -> task + + +def group_albums(session: ImportSession): + """A pipeline stage that groups the items of each task into albums + using their metadata. + + Groups are identified using their artist and album fields. The + pipeline stage emits new album tasks for each discovered group. + """ + + def group(item): + return (item.albumartist or item.artist, item.album) + + task = None + while True: + task = yield task + if task.skip: + continue + tasks = [] + sorted_items: list[library.Item] = sorted(task.items, key=group) + for _, items in itertools.groupby(sorted_items, group): + l_items = list(items) + task = ImportTask(task.toppath, [i.path for i in l_items], l_items) + tasks += task.handle_created(session) + tasks.append(SentinelImportTask(task.toppath, task.paths)) + + task = pipeline.multiple(tasks) + + +@pipeline.mutator_stage +def lookup_candidates(session: ImportSession, task: ImportTask): + """A coroutine for performing the initial MusicBrainz lookup for an + album. It accepts lists of Items and yields + (items, cur_artist, cur_album, candidates, rec) tuples. If no match + is found, all of the yielded parameters (except items) are None. + """ + if task.skip: + # FIXME This gets duplicated a lot. We need a better + # abstraction. + return + + plugins.send("import_task_start", session=session, task=task) + log.debug("Looking up: {0}", displayable_path(task.paths)) + + # Restrict the initial lookup to IDs specified by the user via the -m + # option. Currently all the IDs are passed onto the tasks directly. + task.search_ids = session.config["search_ids"].as_str_seq() + + task.lookup_candidates() + + +@pipeline.stage +def user_query(session: ImportSession, task: ImportTask): + """A coroutine for interfacing with the user about the tagging + process. + + The coroutine accepts an ImportTask objects. It uses the + session's `choose_match` method to determine the `action` for + this task. Depending on the action additional stages are executed + and the processed task is yielded. + + It emits the ``import_task_choice`` event for plugins. Plugins have + access to the choice via the ``task.choice_flag`` property and may + choose to change it. + """ + if task.skip: + return task + + if session.already_merged(task.paths): + return pipeline.BUBBLE + + # Ask the user for a choice. + task.choose_match(session) + plugins.send("import_task_choice", session=session, task=task) + + # As-tracks: transition to singleton workflow. + if task.choice_flag is action.TRACKS: + # Set up a little pipeline for dealing with the singletons. + def emitter(task): + for item in task.items: + task = SingletonImportTask(task.toppath, item) + yield from task.handle_created(session) + yield SentinelImportTask(task.toppath, task.paths) + + return _extend_pipeline( + emitter(task), lookup_candidates(session), user_query(session) + ) + + # As albums: group items by albums and create task for each album + if task.choice_flag is action.ALBUMS: + return _extend_pipeline( + [task], + group_albums(session), + lookup_candidates(session), + user_query(session), + ) + + resolve_duplicates(session, task) + + if task.should_merge_duplicates: + # Create a new task for tagging the current items + # and duplicates together + duplicate_items = task.duplicate_items(session.lib) + + # Duplicates would be reimported so make them look "fresh" + _freshen_items(duplicate_items) + duplicate_paths = [item.path for item in duplicate_items] + + # Record merged paths in the session so they are not reimported + session.mark_merged(duplicate_paths) + + merged_task = ImportTask( + None, task.paths + duplicate_paths, task.items + duplicate_items + ) + + return _extend_pipeline( + [merged_task], lookup_candidates(session), user_query(session) + ) + + apply_choice(session, task) + return task + + +@pipeline.mutator_stage +def import_asis(session: ImportSession, task: ImportTask): + """Select the `action.ASIS` choice for all tasks. + + This stage replaces the initial_lookup and user_query stages + when the importer is run without autotagging. + """ + if task.skip: + return + + log.info("{}", displayable_path(task.paths)) + task.set_choice(action.ASIS) + apply_choice(session, task) + + +@pipeline.mutator_stage +def plugin_stage( + session: ImportSession, + func: Callable[[ImportSession, ImportTask], None], + task: ImportTask, +): + """A coroutine (pipeline stage) that calls the given function with + each non-skipped import task. These stages occur between applying + metadata changes and moving/copying/writing files. + """ + if task.skip: + return + + func(session, task) + + # Stage may modify DB, so re-load cached item data. + # FIXME Importer plugins should not modify the database but instead + # the albums and items attached to tasks. + task.reload() + + +@pipeline.stage +def log_files(session: ImportSession, task: ImportTask): + """A coroutine (pipeline stage) to log each file to be imported.""" + if isinstance(task, SingletonImportTask): + log.info("Singleton: {0}", displayable_path(task.item["path"])) + elif task.items: + log.info("Album: {0}", displayable_path(task.paths[0])) + for item in task.items: + log.info(" {0}", displayable_path(item["path"])) + + +# --------------------------------- Consumer --------------------------------- # +# Anything that should be placed last in the pipeline +# In theory every stage could be a consumer, but in practice there are some +# functions which are typically placed last in the pipeline + + +@pipeline.stage +def manipulate_files(session: ImportSession, task: ImportTask): + """A coroutine (pipeline stage) that performs necessary file + manipulations *after* items have been added to the library and + finalizes each task. + """ + if not task.skip: + if task.should_remove_duplicates: + task.remove_duplicates(session.lib) + + if session.config["move"]: + operation = MoveOperation.MOVE + elif session.config["copy"]: + operation = MoveOperation.COPY + elif session.config["link"]: + operation = MoveOperation.LINK + elif session.config["hardlink"]: + operation = MoveOperation.HARDLINK + elif session.config["reflink"] == "auto": + operation = MoveOperation.REFLINK_AUTO + elif session.config["reflink"]: + operation = MoveOperation.REFLINK + else: + operation = None + + task.manipulate_files( + session=session, + operation=operation, + write=session.config["write"], + ) + + # Progress, cleanup, and event. + task.finalize(session) + + +# ---------------------------- Utility functions ----------------------------- # +# Private functions only used in the stages above + + +def apply_choice(session: ImportSession, task: ImportTask): + """Apply the task's choice to the Album or Item it contains and add + it to the library. + """ + if task.skip: + return + + # Change metadata. + if task.apply: + task.apply_metadata() + plugins.send("import_task_apply", session=session, task=task) + + task.add(session.lib) + + # If ``set_fields`` is set, set those fields to the + # configured values. + # NOTE: This cannot be done before the ``task.add()`` call above, + # because then the ``ImportTask`` won't have an `album` for which + # it can set the fields. + if config["import"]["set_fields"]: + task.set_fields(session.lib) + + +def resolve_duplicates(session: ImportSession, task: ImportTask): + """Check if a task conflicts with items or albums already imported + and ask the session to resolve this. + """ + if task.choice_flag in (action.ASIS, action.APPLY, action.RETAG): + found_duplicates = task.find_duplicates(session.lib) + if found_duplicates: + log.debug( + "found duplicates: {}".format([o.id for o in found_duplicates]) + ) + + # Get the default action to follow from config. + duplicate_action = config["import"]["duplicate_action"].as_choice( + { + "skip": "s", + "keep": "k", + "remove": "r", + "merge": "m", + "ask": "a", + } + ) + log.debug("default action for duplicates: {0}", duplicate_action) + + if duplicate_action == "s": + # Skip new. + task.set_choice(action.SKIP) + elif duplicate_action == "k": + # Keep both. Do nothing; leave the choice intact. + pass + elif duplicate_action == "r": + # Remove old. + task.should_remove_duplicates = True + elif duplicate_action == "m": + # Merge duplicates together + task.should_merge_duplicates = True + else: + # No default action set; ask the session. + session.resolve_duplicate(task, found_duplicates) + + session.log_choice(task, True) + + +def _freshen_items(items): + # Clear IDs from re-tagged items so they appear "fresh" when + # we add them back to the library. + for item in items: + item.id = None + item.album_id = None + + +def _extend_pipeline(tasks, *stages): + # Return pipeline extension for stages with list of tasks + if isinstance(tasks, list): + task_iter = iter(tasks) + else: + task_iter = tasks + + ipl = pipeline.Pipeline([task_iter] + list(stages)) + return pipeline.multiple(ipl.pull()) diff --git a/beets/importer/state.py b/beets/importer/state.py new file mode 100644 index 000000000..fccb7c282 --- /dev/null +++ b/beets/importer/state.py @@ -0,0 +1,142 @@ +# This file is part of beets. +# Copyright 2016, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +from __future__ import annotations + +import logging +import os +import pickle +from bisect import bisect_left, insort +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from beets import config + +if TYPE_CHECKING: + from beets.util import PathBytes + + +# Global logger. +log = logging.getLogger("beets") + + +@dataclass +class ImportState: + """Representing the progress of an import task. + + Opens the state file on creation of the class. If you want + to ensure the state is written to disk, you should use the + context manager protocol. + + Tagprogress allows long tagging tasks to be resumed when they pause. + + Taghistory is a utility for manipulating the "incremental" import log. + This keeps track of all directories that were ever imported, which + allows the importer to only import new stuff. + + Usage + ----- + ``` + # Readonly + progress = ImportState().tagprogress + + # Read and write + with ImportState() as state: + state["key"] = "value" + ``` + """ + + tagprogress: dict[PathBytes, list[PathBytes]] + taghistory: set[tuple[PathBytes, ...]] + path: PathBytes + + def __init__(self, readonly=False, path: PathBytes | None = None): + self.path = path or os.fsencode(config["statefile"].as_filename()) + self.tagprogress = {} + self.taghistory = set() + self._open() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._save() + + def _open( + self, + ): + try: + with open(self.path, "rb") as f: + state = pickle.load(f) + # Read the states + self.tagprogress = state.get("tagprogress", {}) + self.taghistory = state.get("taghistory", set()) + except Exception as exc: + # The `pickle` module can emit all sorts of exceptions during + # unpickling, including ImportError. We use a catch-all + # exception to avoid enumerating them all (the docs don't even have a + # full list!). + log.debug("state file could not be read: {0}", exc) + + def _save(self): + try: + with open(self.path, "wb") as f: + pickle.dump( + { + "tagprogress": self.tagprogress, + "taghistory": self.taghistory, + }, + f, + ) + except OSError as exc: + log.error("state file could not be written: {0}", exc) + + # -------------------------------- Tagprogress ------------------------------- # + + def progress_add(self, toppath: PathBytes, *paths: PathBytes): + """Record that the files under all of the `paths` have been imported + under `toppath`. + """ + with self as state: + imported = state.tagprogress.setdefault(toppath, []) + for path in paths: + if imported and imported[-1] <= path: + imported.append(path) + else: + insort(imported, path) + + def progress_has_element(self, toppath: PathBytes, path: PathBytes) -> bool: + """Return whether `path` has been imported in `toppath`.""" + imported = self.tagprogress.get(toppath, []) + i = bisect_left(imported, path) + return i != len(imported) and imported[i] == path + + def progress_has(self, toppath: PathBytes) -> bool: + """Return `True` if there exist paths that have already been + imported under `toppath`. + """ + return toppath in self.tagprogress + + def progress_reset(self, toppath: PathBytes | None): + """Reset the progress for `toppath`.""" + with self as state: + if toppath in state.tagprogress: + del state.tagprogress[toppath] + + # -------------------------------- Taghistory -------------------------------- # + + def history_add(self, paths: list[PathBytes]): + """Add the paths to the history.""" + with self as state: + state.taghistory.add(tuple(paths)) diff --git a/beets/importer.py b/beets/importer/tasks.py similarity index 60% rename from beets/importer.py rename to beets/importer/tasks.py index 2bdb16669..2d3dc44e8 100644 --- a/beets/importer.py +++ b/beets/importer/tasks.py @@ -12,44 +12,31 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Provides the basic, interface-agnostic workflow for importing and -autotagging music files. -""" - from __future__ import annotations -import itertools +import logging import os -import pickle import re import shutil import time -from bisect import bisect_left, insort from collections import defaultdict -from dataclasses import dataclass from enum import Enum from tempfile import mkdtemp -from typing import Callable, Iterable, Sequence +from typing import TYPE_CHECKING, Callable, Iterable, Sequence import mediafile -from beets import autotag, config, dbcore, library, logging, plugins, util -from beets.util import ( - MoveOperation, - ancestry, - displayable_path, - normpath, - pipeline, - sorted_walk, - syspath, -) +from beets import autotag, config, dbcore, library, plugins, util + +from .state import ImportState + +if TYPE_CHECKING: + from .session import ImportSession + +# Global logger. +log = logging.getLogger("beets") -action = Enum("action", ["SKIP", "ASIS", "TRACKS", "APPLY", "ALBUMS", "RETAG"]) -# The RETAG action represents "don't apply any match, but do record -# new metadata". It's not reachable via the standard command prompt but -# can be used by plugins. -QUEUE_SIZE = 128 SINGLE_ARTIST_THRESH = 0.25 # Usually flexible attributes are preserved (i.e., not updated) during @@ -74,9 +61,11 @@ REIMPORT_FRESH_FIELDS_ITEM = list(REIMPORT_FRESH_FIELDS_ALBUM) # Global logger. log = logging.getLogger("beets") -# Here for now to allow for a easy replace later on -# once we can move to a PathLike -PathBytes = bytes + +action = Enum("action", ["SKIP", "ASIS", "TRACKS", "APPLY", "ALBUMS", "RETAG"]) +# The RETAG action represents "don't apply any match, but do record +# new metadata". It's not reachable via the standard command prompt but +# can be used by plugins. class ImportAbortError(Exception): @@ -85,395 +74,20 @@ class ImportAbortError(Exception): pass -@dataclass -class ImportState: - """Representing the progress of an import task. - - Opens the state file on creation of the class. If you want - to ensure the state is written to disk, you should use the - context manager protocol. - - Tagprogress allows long tagging tasks to be resumed when they pause. - - Taghistory is a utility for manipulating the "incremental" import log. - This keeps track of all directories that were ever imported, which - allows the importer to only import new stuff. - - Usage - ----- - ``` - # Readonly - progress = ImportState().tagprogress - - # Read and write - with ImportState() as state: - state["key"] = "value" - ``` - """ - - tagprogress: dict[PathBytes, list[PathBytes]] - taghistory: set[tuple[PathBytes, ...]] - path: PathBytes - - def __init__(self, readonly=False, path: PathBytes | None = None): - self.path = path or os.fsencode(config["statefile"].as_filename()) - self.tagprogress = {} - self.taghistory = set() - self._open() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self._save() - - def _open( - self, - ): - try: - with open(self.path, "rb") as f: - state = pickle.load(f) - # Read the states - self.tagprogress = state.get("tagprogress", {}) - self.taghistory = state.get("taghistory", set()) - except Exception as exc: - # The `pickle` module can emit all sorts of exceptions during - # unpickling, including ImportError. We use a catch-all - # exception to avoid enumerating them all (the docs don't even have a - # full list!). - log.debug("state file could not be read: {0}", exc) - - def _save(self): - try: - with open(self.path, "wb") as f: - pickle.dump( - { - "tagprogress": self.tagprogress, - "taghistory": self.taghistory, - }, - f, - ) - except OSError as exc: - log.error("state file could not be written: {0}", exc) - - # -------------------------------- Tagprogress ------------------------------- # - - def progress_add(self, toppath: PathBytes, *paths: PathBytes): - """Record that the files under all of the `paths` have been imported - under `toppath`. - """ - with self as state: - imported = state.tagprogress.setdefault(toppath, []) - for path in paths: - if imported and imported[-1] <= path: - imported.append(path) - else: - insort(imported, path) - - def progress_has_element(self, toppath: PathBytes, path: PathBytes) -> bool: - """Return whether `path` has been imported in `toppath`.""" - imported = self.tagprogress.get(toppath, []) - i = bisect_left(imported, path) - return i != len(imported) and imported[i] == path - - def progress_has(self, toppath: PathBytes) -> bool: - """Return `True` if there exist paths that have already been - imported under `toppath`. - """ - return toppath in self.tagprogress - - def progress_reset(self, toppath: PathBytes | None): - """Reset the progress for `toppath`.""" - with self as state: - if toppath in state.tagprogress: - del state.tagprogress[toppath] - - # -------------------------------- Taghistory -------------------------------- # - - def history_add(self, paths: list[PathBytes]): - """Add the paths to the history.""" - with self as state: - state.taghistory.add(tuple(paths)) - - -class ImportSession: - """Controls an import action. Subclasses should implement methods to - communicate with the user or otherwise make decisions. - """ - - logger: logging.Logger - paths: list[PathBytes] - lib: library.Library - - _is_resuming: dict[bytes, bool] - _merged_items: set[PathBytes] - _merged_dirs: set[PathBytes] - - def __init__( - self, - lib: library.Library, - loghandler: logging.Handler | None, - paths: Sequence[PathBytes] | None, - query: dbcore.Query | None, - ): - """Create a session. - - Parameters - ---------- - lib : library.Library - The library instance to which items will be imported. - loghandler : logging.Handler or None - A logging handler to use for the session's logger. If None, a - NullHandler will be used. - paths : os.PathLike or None - The paths to be imported. - query : dbcore.Query or None - A query to filter items for import. - """ - self.lib = lib - self.logger = self._setup_logging(loghandler) - self.query = query - self._is_resuming = {} - self._merged_items = set() - self._merged_dirs = set() - - # Normalize the paths. - self.paths = list(map(normpath, paths or [])) - - def _setup_logging(self, loghandler: logging.Handler | None): - logger = logging.getLogger(__name__) - logger.propagate = False - if not loghandler: - loghandler = logging.NullHandler() - logger.handlers = [loghandler] - return logger - - def set_config(self, config): - """Set `config` property from global import config and make - implied changes. - """ - # FIXME: Maybe this function should not exist and should instead - # provide "decision wrappers" like "should_resume()", etc. - iconfig = dict(config) - self.config = iconfig - - # Incremental and progress are mutually exclusive. - if iconfig["incremental"]: - iconfig["resume"] = False - - # When based on a query instead of directories, never - # save progress or try to resume. - if self.query is not None: - iconfig["resume"] = False - iconfig["incremental"] = False - - if iconfig["reflink"]: - iconfig["reflink"] = iconfig["reflink"].as_choice( - ["auto", True, False] - ) - - # Copy, move, reflink, link, and hardlink are mutually exclusive. - if iconfig["move"]: - iconfig["copy"] = False - iconfig["link"] = False - iconfig["hardlink"] = False - iconfig["reflink"] = False - elif iconfig["link"]: - iconfig["copy"] = False - iconfig["move"] = False - iconfig["hardlink"] = False - iconfig["reflink"] = False - elif iconfig["hardlink"]: - iconfig["copy"] = False - iconfig["move"] = False - iconfig["link"] = False - iconfig["reflink"] = False - elif iconfig["reflink"]: - iconfig["copy"] = False - iconfig["move"] = False - iconfig["link"] = False - iconfig["hardlink"] = False - - # Only delete when copying. - if not iconfig["copy"]: - iconfig["delete"] = False - - self.want_resume = config["resume"].as_choice([True, False, "ask"]) - - def tag_log(self, status, paths: Sequence[PathBytes]): - """Log a message about a given album to the importer log. The status - should reflect the reason the album couldn't be tagged. - """ - self.logger.info("{0} {1}", status, displayable_path(paths)) - - def log_choice(self, task: ImportTask, duplicate=False): - """Logs the task's current choice if it should be logged. If - ``duplicate``, then this is a secondary choice after a duplicate was - detected and a decision was made. - """ - paths = task.paths - if duplicate: - # Duplicate: log all three choices (skip, keep both, and trump). - if task.should_remove_duplicates: - self.tag_log("duplicate-replace", paths) - elif task.choice_flag in (action.ASIS, action.APPLY): - self.tag_log("duplicate-keep", paths) - elif task.choice_flag is (action.SKIP): - self.tag_log("duplicate-skip", paths) - else: - # Non-duplicate: log "skip" and "asis" choices. - if task.choice_flag is action.ASIS: - self.tag_log("asis", paths) - elif task.choice_flag is action.SKIP: - self.tag_log("skip", paths) - - def should_resume(self, path: PathBytes): - raise NotImplementedError - - def choose_match(self, task: ImportTask): - raise NotImplementedError - - def resolve_duplicate(self, task: ImportTask, found_duplicates): - raise NotImplementedError - - def choose_item(self, task: ImportTask): - raise NotImplementedError - - def run(self): - """Run the import task.""" - self.logger.info("import started {0}", time.asctime()) - self.set_config(config["import"]) - - # Set up the pipeline. - if self.query is None: - stages = [read_tasks(self)] - else: - stages = [query_tasks(self)] - - # In pretend mode, just log what would otherwise be imported. - if self.config["pretend"]: - stages += [log_files(self)] - else: - if self.config["group_albums"] and not self.config["singletons"]: - # Split directory tasks into one task for each album. - stages += [group_albums(self)] - - # These stages either talk to the user to get a decision or, - # in the case of a non-autotagged import, just choose to - # import everything as-is. In *both* cases, these stages - # also add the music to the library database, so later - # stages need to read and write data from there. - if self.config["autotag"]: - stages += [lookup_candidates(self), user_query(self)] - else: - stages += [import_asis(self)] - - # Plugin stages. - for stage_func in plugins.early_import_stages(): - stages.append(plugin_stage(self, stage_func)) - for stage_func in plugins.import_stages(): - stages.append(plugin_stage(self, stage_func)) - - stages += [manipulate_files(self)] - - pl = pipeline.Pipeline(stages) - - # Run the pipeline. - plugins.send("import_begin", session=self) - try: - if config["threaded"]: - pl.run_parallel(QUEUE_SIZE) - else: - pl.run_sequential() - except ImportAbortError: - # User aborted operation. Silently stop. - pass - - # Incremental and resumed imports - - def already_imported(self, toppath: PathBytes, paths: Sequence[PathBytes]): - """Returns true if the files belonging to this task have already - been imported in a previous session. - """ - if self.is_resuming(toppath) and all( - [ImportState().progress_has_element(toppath, p) for p in paths] - ): - return True - if self.config["incremental"] and tuple(paths) in self.history_dirs: - return True - - return False - - _history_dirs = None - - @property - def history_dirs(self) -> set[tuple[PathBytes, ...]]: - # FIXME: This could be simplified to a cached property - if self._history_dirs is None: - self._history_dirs = ImportState().taghistory - return self._history_dirs - - def already_merged(self, paths: Sequence[PathBytes]): - """Returns true if all the paths being imported were part of a merge - during previous tasks. - """ - for path in paths: - if path not in self._merged_items and path not in self._merged_dirs: - return False - return True - - def mark_merged(self, paths: Sequence[PathBytes]): - """Mark paths and directories as merged for future reimport tasks.""" - self._merged_items.update(paths) - dirs = { - os.path.dirname(path) if os.path.isfile(syspath(path)) else path - for path in paths - } - self._merged_dirs.update(dirs) - - def is_resuming(self, toppath: PathBytes): - """Return `True` if user wants to resume import of this path. - - You have to call `ask_resume` first to determine the return value. - """ - return self._is_resuming.get(toppath, False) - - def ask_resume(self, toppath: PathBytes): - """If import of `toppath` was aborted in an earlier session, ask - user if they want to resume the import. - - Determines the return value of `is_resuming(toppath)`. - """ - if self.want_resume and ImportState().progress_has(toppath): - # Either accept immediately or prompt for input to decide. - if self.want_resume is True or self.should_resume(toppath): - log.warning( - "Resuming interrupted import of {0}", - util.displayable_path(toppath), - ) - self._is_resuming[toppath] = True - else: - # Clear progress; we're starting from the top. - ImportState().progress_reset(toppath) - - -# The importer task class. - - class BaseImportTask: """An abstract base class for importer tasks. Tasks flow through the importer pipeline. Each stage can update them.""" - toppath: PathBytes | None - paths: list[PathBytes] + toppath: util.PathBytes | None + paths: list[util.PathBytes] items: list[library.Item] def __init__( self, - toppath: PathBytes | None, - paths: Iterable[PathBytes] | None, + toppath: util.PathBytes | None, + paths: Iterable[util.PathBytes] | None, items: Iterable[library.Item] | None, ): """Create a task. The primary fields that define a task are: @@ -539,8 +153,8 @@ class ImportTask(BaseImportTask): def __init__( self, - toppath: PathBytes | None, - paths: Iterable[PathBytes] | None, + toppath: util.PathBytes | None, + paths: Iterable[util.PathBytes] | None, items: Iterable[library.Item] | None, ): super().__init__(toppath, paths, items) @@ -662,7 +276,7 @@ class ImportTask(BaseImportTask): value = str(view.get()) log.debug( "Set field {1}={2} for {0}", - displayable_path(self.paths), + util.displayable_path(self.paths), field, value, ) @@ -823,7 +437,7 @@ class ImportTask(BaseImportTask): def manipulate_files( self, session: ImportSession, - operation: MoveOperation | None = None, + operation: util.MoveOperation | None = None, write=False, ): """Copy, move, link, hardlink or reflink (depending on `operation`) @@ -838,7 +452,7 @@ class ImportTask(BaseImportTask): items = self.imported_items() # Save the original paths of all items for deletion and pruning # in the next step (finalization). - self.old_paths: list[PathBytes] = [item.path for item in items] + self.old_paths: list[util.PathBytes] = [item.path for item in items] for item in items: if operation is not None: # In copy and link modes, treat re-imports specially: @@ -846,7 +460,7 @@ class ImportTask(BaseImportTask): # copied/moved as usual). old_path = item.path if ( - operation != MoveOperation.MOVE + operation != util.MoveOperation.MOVE and self.replaced_items[item] and session.lib.directory in util.ancestry(old_path) ): @@ -893,11 +507,13 @@ class ImportTask(BaseImportTask): and `replaced_albums` dictionaries. """ self.replaced_items = defaultdict(list) - self.replaced_albums: dict[PathBytes, library.Album] = defaultdict() + self.replaced_albums: dict[util.PathBytes, library.Album] = ( + defaultdict() + ) replaced_album_ids = set() for item in self.imported_items(): dup_items = list( - lib.items(dbcore.query.BytesQuery("path", item.path)) + lib.items(query=dbcore.query.BytesQuery("path", item.path)) ) self.replaced_items[item] = dup_items for dup_item in dup_items: @@ -938,7 +554,7 @@ class ImportTask(BaseImportTask): noun, new_obj.id, overwritten_fields, - displayable_path(new_obj.path), + util.displayable_path(new_obj.path), ) for key in overwritten_fields: del existing_fields[key] @@ -960,14 +576,14 @@ class ImportTask(BaseImportTask): "Reimported album {}. Preserving attribute ['added']. " "Path: {}", self.album.id, - displayable_path(self.album.path), + util.displayable_path(self.album.path), ) log.debug( "Reimported album {}. Preserving flexible attributes {}. " "Path: {}", self.album.id, list(album_fields.keys()), - displayable_path(self.album.path), + util.displayable_path(self.album.path), ) for item in self.imported_items(): @@ -979,7 +595,7 @@ class ImportTask(BaseImportTask): "Reimported item {}. Preserving attribute ['added']. " "Path: {}", item.id, - displayable_path(item.path), + util.displayable_path(item.path), ) item_fields = _reduce_and_log( item, dup_item._values_flex, REIMPORT_FRESH_FIELDS_ITEM @@ -990,7 +606,7 @@ class ImportTask(BaseImportTask): "Path: {}", item.id, list(item_fields.keys()), - displayable_path(item.path), + util.displayable_path(item.path), ) item.store() @@ -1003,7 +619,7 @@ class ImportTask(BaseImportTask): log.debug( "Replacing item {0}: {1}", dup_item.id, - displayable_path(item.path), + util.displayable_path(item.path), ) dup_item.remove() log.debug( @@ -1033,7 +649,7 @@ class ImportTask(BaseImportTask): the file still exists, no pruning is performed, so it's safe to call when the file in question may not have been removed. """ - if self.toppath and not os.path.exists(syspath(filename)): + if self.toppath and not os.path.exists(util.syspath(filename)): util.prune_dirs( os.path.dirname(filename), self.toppath, @@ -1044,7 +660,7 @@ class ImportTask(BaseImportTask): class SingletonImportTask(ImportTask): """ImportTask for a single track that is not associated to an album.""" - def __init__(self, toppath: PathBytes | None, item: library.Item): + def __init__(self, toppath: util.PathBytes | None, item: library.Item): super().__init__(toppath, [item.path], [item]) self.item = item self.is_album = False @@ -1127,7 +743,7 @@ class SingletonImportTask(ImportTask): value = str(view.get()) log.debug( "Set field {1}={2} for {0}", - displayable_path(self.paths), + util.displayable_path(self.paths), field, value, ) @@ -1246,9 +862,9 @@ class ArchiveImportTask(SentinelImportTask): if self.extracted and self.toppath: log.debug( "Removing extracted directory: {0}", - displayable_path(self.toppath), + util.displayable_path(self.toppath), ) - shutil.rmtree(syspath(self.toppath)) + shutil.rmtree(util.syspath(self.toppath)) def extract(self): """Extracts the archive to a temporary directory and sets @@ -1289,7 +905,7 @@ class ImportTaskFactory: indicated by a path. """ - def __init__(self, toppath: PathBytes, session: ImportSession): + def __init__(self, toppath: util.PathBytes, session: ImportSession): """Create a new task factory. `toppath` is the user-specified path to search for music to @@ -1300,7 +916,7 @@ class ImportTaskFactory: self.session = session self.skipped = 0 # Skipped due to incremental/resume. self.imported = 0 # "Real" tasks created. - self.is_archive = ArchiveImportTask.is_archive(syspath(toppath)) + self.is_archive = ArchiveImportTask.is_archive(util.syspath(toppath)) def tasks(self): """Yield all import tasks for music found in the user-specified @@ -1362,7 +978,7 @@ class ImportTaskFactory: single track when `toppath` is a file, a single directory in `flat` mode. """ - if not os.path.isdir(syspath(self.toppath)): + if not os.path.isdir(util.syspath(self.toppath)): yield [self.toppath], [self.toppath] elif self.session.config["flat"]: paths = [] @@ -1373,11 +989,12 @@ class ImportTaskFactory: for dirs, paths in albums_in_dir(self.toppath): yield dirs, paths - def singleton(self, path: PathBytes): + def singleton(self, path: util.PathBytes): """Return a `SingletonImportTask` for the music file.""" if self.session.already_imported(self.toppath, [path]): log.debug( - "Skipping previously-imported path: {0}", displayable_path(path) + "Skipping previously-imported path: {0}", + util.displayable_path(path), ) self.skipped += 1 return None @@ -1388,7 +1005,7 @@ class ImportTaskFactory: else: return None - def album(self, paths: Iterable[PathBytes], dirs=None): + def album(self, paths: Iterable[util.PathBytes], dirs=None): """Return a `ImportTask` with all media files from paths. `dirs` is a list of parent directories used to record already @@ -1400,7 +1017,8 @@ class ImportTaskFactory: if self.session.already_imported(self.toppath, dirs): log.debug( - "Skipping previously-imported path: {0}", displayable_path(dirs) + "Skipping previously-imported path: {0}", + util.displayable_path(dirs), ) self.skipped += 1 return None @@ -1414,7 +1032,7 @@ class ImportTaskFactory: else: return None - def sentinel(self, paths: Iterable[PathBytes] | None = None): + def sentinel(self, paths: Iterable[util.PathBytes] | None = None): """Return a `SentinelImportTask` indicating the end of a top-level directory import. """ @@ -1436,7 +1054,9 @@ class ImportTaskFactory: ) return - log.debug("Extracting archive: {0}", displayable_path(self.toppath)) + log.debug( + "Extracting archive: {0}", util.displayable_path(self.toppath) + ) archive_task = ArchiveImportTask(self.toppath) try: archive_task.extract() @@ -1449,7 +1069,7 @@ class ImportTaskFactory: log.debug("Archive extracted to: {0}", self.toppath) return archive_task - def read_item(self, path: PathBytes): + def read_item(self, path: util.PathBytes): """Return an `Item` read from the path. If an item cannot be read, return `None` instead and log an @@ -1462,355 +1082,11 @@ class ImportTaskFactory: # Silently ignore non-music files. pass elif isinstance(exc.reason, mediafile.UnreadableFileError): - log.warning("unreadable file: {0}", displayable_path(path)) + log.warning("unreadable file: {0}", util.displayable_path(path)) else: - log.error("error reading {0}: {1}", displayable_path(path), exc) - - -# Pipeline utilities - - -def _freshen_items(items): - # Clear IDs from re-tagged items so they appear "fresh" when - # we add them back to the library. - for item in items: - item.id = None - item.album_id = None - - -def _extend_pipeline(tasks, *stages): - # Return pipeline extension for stages with list of tasks - if isinstance(tasks, list): - task_iter = iter(tasks) - else: - task_iter = tasks - - ipl = pipeline.Pipeline([task_iter] + list(stages)) - return pipeline.multiple(ipl.pull()) - - -# Full-album pipeline stages. - - -def read_tasks(session: ImportSession): - """A generator yielding all the albums (as ImportTask objects) found - in the user-specified list of paths. In the case of a singleton - import, yields single-item tasks instead. - """ - skipped = 0 - - for toppath in session.paths: - # Check whether we need to resume the import. - session.ask_resume(toppath) - - # Generate tasks. - task_factory = ImportTaskFactory(toppath, session) - yield from task_factory.tasks() - skipped += task_factory.skipped - - if not task_factory.imported: - log.warning("No files imported from {0}", displayable_path(toppath)) - - # Show skipped directories (due to incremental/resume). - if skipped: - log.info("Skipped {0} paths.", skipped) - - -def query_tasks(session: ImportSession): - """A generator that works as a drop-in-replacement for read_tasks. - Instead of finding files from the filesystem, a query is used to - match items from the library. - """ - if session.config["singletons"]: - # Search for items. - for item in session.lib.items(session.query): - task = SingletonImportTask(None, item) - for task in task.handle_created(session): - yield task - - else: - # Search for albums. - for album in session.lib.albums(session.query): - log.debug( - "yielding album {0}: {1} - {2}", - album.id, - album.albumartist, - album.album, - ) - items = list(album.items()) - _freshen_items(items) - - task = ImportTask(None, [album.item_dir()], items) - for task in task.handle_created(session): - yield task - - -@pipeline.mutator_stage -def lookup_candidates(session: ImportSession, task: ImportTask): - """A coroutine for performing the initial MusicBrainz lookup for an - album. It accepts lists of Items and yields - (items, cur_artist, cur_album, candidates, rec) tuples. If no match - is found, all of the yielded parameters (except items) are None. - """ - if task.skip: - # FIXME This gets duplicated a lot. We need a better - # abstraction. - return - - plugins.send("import_task_start", session=session, task=task) - log.debug("Looking up: {0}", displayable_path(task.paths)) - - # Restrict the initial lookup to IDs specified by the user via the -m - # option. Currently all the IDs are passed onto the tasks directly. - task.search_ids = session.config["search_ids"].as_str_seq() - - task.lookup_candidates() - - -@pipeline.stage -def user_query(session: ImportSession, task: ImportTask): - """A coroutine for interfacing with the user about the tagging - process. - - The coroutine accepts an ImportTask objects. It uses the - session's `choose_match` method to determine the `action` for - this task. Depending on the action additional stages are executed - and the processed task is yielded. - - It emits the ``import_task_choice`` event for plugins. Plugins have - access to the choice via the ``task.choice_flag`` property and may - choose to change it. - """ - if task.skip: - return task - - if session.already_merged(task.paths): - return pipeline.BUBBLE - - # Ask the user for a choice. - task.choose_match(session) - plugins.send("import_task_choice", session=session, task=task) - - # As-tracks: transition to singleton workflow. - if task.choice_flag is action.TRACKS: - # Set up a little pipeline for dealing with the singletons. - def emitter(task): - for item in task.items: - task = SingletonImportTask(task.toppath, item) - yield from task.handle_created(session) - yield SentinelImportTask(task.toppath, task.paths) - - return _extend_pipeline( - emitter(task), lookup_candidates(session), user_query(session) - ) - - # As albums: group items by albums and create task for each album - if task.choice_flag is action.ALBUMS: - return _extend_pipeline( - [task], - group_albums(session), - lookup_candidates(session), - user_query(session), - ) - - resolve_duplicates(session, task) - - if task.should_merge_duplicates: - # Create a new task for tagging the current items - # and duplicates together - duplicate_items = task.duplicate_items(session.lib) - - # Duplicates would be reimported so make them look "fresh" - _freshen_items(duplicate_items) - duplicate_paths = [item.path for item in duplicate_items] - - # Record merged paths in the session so they are not reimported - session.mark_merged(duplicate_paths) - - merged_task = ImportTask( - None, task.paths + duplicate_paths, task.items + duplicate_items - ) - - return _extend_pipeline( - [merged_task], lookup_candidates(session), user_query(session) - ) - - apply_choice(session, task) - return task - - -def resolve_duplicates(session: ImportSession, task: ImportTask): - """Check if a task conflicts with items or albums already imported - and ask the session to resolve this. - """ - if task.choice_flag in (action.ASIS, action.APPLY, action.RETAG): - found_duplicates = task.find_duplicates(session.lib) - if found_duplicates: - log.debug( - "found duplicates: {}".format([o.id for o in found_duplicates]) - ) - - # Get the default action to follow from config. - duplicate_action = config["import"]["duplicate_action"].as_choice( - { - "skip": "s", - "keep": "k", - "remove": "r", - "merge": "m", - "ask": "a", - } - ) - log.debug("default action for duplicates: {0}", duplicate_action) - - if duplicate_action == "s": - # Skip new. - task.set_choice(action.SKIP) - elif duplicate_action == "k": - # Keep both. Do nothing; leave the choice intact. - pass - elif duplicate_action == "r": - # Remove old. - task.should_remove_duplicates = True - elif duplicate_action == "m": - # Merge duplicates together - task.should_merge_duplicates = True - else: - # No default action set; ask the session. - session.resolve_duplicate(task, found_duplicates) - - session.log_choice(task, True) - - -@pipeline.mutator_stage -def import_asis(session: ImportSession, task: ImportTask): - """Select the `action.ASIS` choice for all tasks. - - This stage replaces the initial_lookup and user_query stages - when the importer is run without autotagging. - """ - if task.skip: - return - - log.info("{}", displayable_path(task.paths)) - task.set_choice(action.ASIS) - apply_choice(session, task) - - -def apply_choice(session: ImportSession, task: ImportTask): - """Apply the task's choice to the Album or Item it contains and add - it to the library. - """ - if task.skip: - return - - # Change metadata. - if task.apply: - task.apply_metadata() - plugins.send("import_task_apply", session=session, task=task) - - task.add(session.lib) - - # If ``set_fields`` is set, set those fields to the - # configured values. - # NOTE: This cannot be done before the ``task.add()`` call above, - # because then the ``ImportTask`` won't have an `album` for which - # it can set the fields. - if config["import"]["set_fields"]: - task.set_fields(session.lib) - - -@pipeline.mutator_stage -def plugin_stage( - session: ImportSession, - func: Callable[[ImportSession, ImportTask], None], - task: ImportTask, -): - """A coroutine (pipeline stage) that calls the given function with - each non-skipped import task. These stages occur between applying - metadata changes and moving/copying/writing files. - """ - if task.skip: - return - - func(session, task) - - # Stage may modify DB, so re-load cached item data. - # FIXME Importer plugins should not modify the database but instead - # the albums and items attached to tasks. - task.reload() - - -@pipeline.stage -def manipulate_files(session: ImportSession, task: ImportTask): - """A coroutine (pipeline stage) that performs necessary file - manipulations *after* items have been added to the library and - finalizes each task. - """ - if not task.skip: - if task.should_remove_duplicates: - task.remove_duplicates(session.lib) - - if session.config["move"]: - operation = MoveOperation.MOVE - elif session.config["copy"]: - operation = MoveOperation.COPY - elif session.config["link"]: - operation = MoveOperation.LINK - elif session.config["hardlink"]: - operation = MoveOperation.HARDLINK - elif session.config["reflink"] == "auto": - operation = MoveOperation.REFLINK_AUTO - elif session.config["reflink"]: - operation = MoveOperation.REFLINK - else: - operation = None - - task.manipulate_files( - session=session, - operation=operation, - write=session.config["write"], - ) - - # Progress, cleanup, and event. - task.finalize(session) - - -@pipeline.stage -def log_files(session: ImportSession, task: ImportTask): - """A coroutine (pipeline stage) to log each file to be imported.""" - if isinstance(task, SingletonImportTask): - log.info("Singleton: {0}", displayable_path(task.item["path"])) - elif task.items: - log.info("Album: {0}", displayable_path(task.paths[0])) - for item in task.items: - log.info(" {0}", displayable_path(item["path"])) - - -def group_albums(session: ImportSession): - """A pipeline stage that groups the items of each task into albums - using their metadata. - - Groups are identified using their artist and album fields. The - pipeline stage emits new album tasks for each discovered group. - """ - - def group(item): - return (item.albumartist or item.artist, item.album) - - task = None - while True: - task = yield task - if task.skip: - continue - tasks = [] - sorted_items: list[library.Item] = sorted(task.items, key=group) - for _, items in itertools.groupby(sorted_items, group): - l_items = list(items) - task = ImportTask(task.toppath, [i.path for i in l_items], l_items) - tasks += task.handle_created(session) - tasks.append(SentinelImportTask(task.toppath, task.paths)) - - task = pipeline.multiple(tasks) + log.error( + "error reading {0}: {1}", util.displayable_path(path), exc + ) MULTIDISC_MARKERS = (rb"dis[ck]", rb"cd") @@ -1821,11 +1097,11 @@ def is_subdir_of_any_in_list(path, dirs): """Returns True if path os a subdirectory of any directory in dirs (a list). In other case, returns False. """ - ancestors = ancestry(path) + ancestors = util.ancestry(path) return any(d in ancestors for d in dirs) -def albums_in_dir(path: PathBytes): +def albums_in_dir(path: util.PathBytes): """Recursively searches the given directory and returns an iterable of (paths, items) where paths is a list of directories and items is a list of Items that is probably an album. Specifically, any folder @@ -1835,7 +1111,7 @@ def albums_in_dir(path: PathBytes): ignore: list[str] = config["ignore"].as_str_seq() ignore_hidden: bool = config["ignore_hidden"].get(bool) - for root, dirs, files in sorted_walk( + for root, dirs, files in util.sorted_walk( path, ignore=ignore, ignore_hidden=ignore_hidden, logger=log ): items = [os.path.join(root, f) for f in files] From 68acaa6470a04474a2aefe9b1de71ae7fa10b861 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr <sebastian@mohrenclan.de> Date: Tue, 13 May 2025 12:11:40 +0200 Subject: [PATCH 040/728] Renamed all `action` occurrences with `Action`. --- beets/importer/__init__.py | 38 +++++++++ beets/importer/session.py | 33 ++++---- beets/importer/stages.py | 22 ++--- beets/importer/tasks.py | 66 ++++++++------- beets/test/helper.py | 24 +++--- beets/ui/commands.py | 38 ++++----- beets/util/__init__.py | 4 + beetsplug/badfiles.py | 2 +- beetsplug/edit.py | 6 +- beetsplug/fetchart.py | 6 +- beetsplug/ihate.py | 6 +- beetsplug/zero.py | 4 +- docs/dev/plugins.rst | 2 +- test/plugins/test_importadded.py | 2 +- test/test_importer.py | 138 +++++++++++++++---------------- test/test_plugins.py | 10 +-- 16 files changed, 227 insertions(+), 174 deletions(-) create mode 100644 beets/importer/__init__.py diff --git a/beets/importer/__init__.py b/beets/importer/__init__.py new file mode 100644 index 000000000..586b238e6 --- /dev/null +++ b/beets/importer/__init__.py @@ -0,0 +1,38 @@ +# This file is part of beets. +# Copyright 2016, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Provides the basic, interface-agnostic workflow for importing and +autotagging music files. +""" + +from .session import ImportAbortError, ImportSession +from .tasks import ( + Action, + ArchiveImportTask, + ImportTask, + SentinelImportTask, + SingletonImportTask, +) + +# Note: Stages are not exposed to the public API + +__all__ = [ + "ImportSession", + "ImportAbortError", + "Action", + "ImportTask", + "ArchiveImportTask", + "SentinelImportTask", + "SingletonImportTask", +] diff --git a/beets/importer/session.py b/beets/importer/session.py index 620eb688e..e45644fa3 100644 --- a/beets/importer/session.py +++ b/beets/importer/session.py @@ -18,10 +18,10 @@ import time from typing import TYPE_CHECKING, Sequence from beets import config, dbcore, library, logging, plugins, util -from beets.importer.tasks import action +from beets.importer.tasks import Action from beets.util import displayable_path, normpath, pipeline, syspath -from .stages import * +from . import stages as stagefuncs from .state import ImportState if TYPE_CHECKING: @@ -162,15 +162,15 @@ class ImportSession: # Duplicate: log all three choices (skip, keep both, and trump). if task.should_remove_duplicates: self.tag_log("duplicate-replace", paths) - elif task.choice_flag in (action.ASIS, action.APPLY): + elif task.choice_flag in (Action.ASIS, Action.APPLY): self.tag_log("duplicate-keep", paths) - elif task.choice_flag is action.SKIP: + elif task.choice_flag is Action.SKIP: self.tag_log("duplicate-skip", paths) else: # Non-duplicate: log "skip" and "asis" choices. - if task.choice_flag is action.ASIS: + if task.choice_flag is Action.ASIS: self.tag_log("asis", paths) - elif task.choice_flag is action.SKIP: + elif task.choice_flag is Action.SKIP: self.tag_log("skip", paths) def should_resume(self, path: PathBytes): @@ -192,17 +192,17 @@ class ImportSession: # Set up the pipeline. if self.query is None: - stages = [read_tasks(self)] + stages = [stagefuncs.read_tasks(self)] else: - stages = [query_tasks(self)] + stages = [stagefuncs.query_tasks(self)] # In pretend mode, just log what would otherwise be imported. if self.config["pretend"]: - stages += [log_files(self)] + stages += [stagefuncs.log_files(self)] else: if self.config["group_albums"] and not self.config["singletons"]: # Split directory tasks into one task for each album. - stages += [group_albums(self)] + stages += [stagefuncs.group_albums(self)] # These stages either talk to the user to get a decision or, # in the case of a non-autotagged import, just choose to @@ -210,17 +210,20 @@ class ImportSession: # also add the music to the library database, so later # stages need to read and write data from there. if self.config["autotag"]: - stages += [lookup_candidates(self), user_query(self)] + stages += [ + stagefuncs.lookup_candidates(self), + stagefuncs.user_query(self), + ] else: - stages += [import_asis(self)] + stages += [stagefuncs.import_asis(self)] # Plugin stages. for stage_func in plugins.early_import_stages(): - stages.append(plugin_stage(self, stage_func)) + stages.append(stagefuncs.plugin_stage(self, stage_func)) for stage_func in plugins.import_stages(): - stages.append(plugin_stage(self, stage_func)) + stages.append(stagefuncs.plugin_stage(self, stage_func)) - stages += [manipulate_files(self)] + stages += [stagefuncs.manipulate_files(self)] pl = pipeline.Pipeline(stages) diff --git a/beets/importer/stages.py b/beets/importer/stages.py index 52b2a221a..5b3540db4 100644 --- a/beets/importer/stages.py +++ b/beets/importer/stages.py @@ -22,7 +22,7 @@ from beets import config, plugins from beets.util import MoveOperation, displayable_path, pipeline from .tasks import ( - action, + Action, ImportTask, ImportTaskFactory, SentinelImportTask, @@ -173,7 +173,7 @@ def user_query(session: ImportSession, task: ImportTask): plugins.send("import_task_choice", session=session, task=task) # As-tracks: transition to singleton workflow. - if task.choice_flag is action.TRACKS: + if task.choice_flag is Action.TRACKS: # Set up a little pipeline for dealing with the singletons. def emitter(task): for item in task.items: @@ -186,7 +186,7 @@ def user_query(session: ImportSession, task: ImportTask): ) # As albums: group items by albums and create task for each album - if task.choice_flag is action.ALBUMS: + if task.choice_flag is Action.ALBUMS: return _extend_pipeline( [task], group_albums(session), @@ -194,7 +194,7 @@ def user_query(session: ImportSession, task: ImportTask): user_query(session), ) - resolve_duplicates(session, task) + _resolve_duplicates(session, task) if task.should_merge_duplicates: # Create a new task for tagging the current items @@ -216,7 +216,7 @@ def user_query(session: ImportSession, task: ImportTask): [merged_task], lookup_candidates(session), user_query(session) ) - apply_choice(session, task) + _apply_choice(session, task) return task @@ -231,8 +231,8 @@ def import_asis(session: ImportSession, task: ImportTask): return log.info("{}", displayable_path(task.paths)) - task.set_choice(action.ASIS) - apply_choice(session, task) + task.set_choice(Action.ASIS) + _apply_choice(session, task) @pipeline.mutator_stage @@ -312,7 +312,7 @@ def manipulate_files(session: ImportSession, task: ImportTask): # Private functions only used in the stages above -def apply_choice(session: ImportSession, task: ImportTask): +def _apply_choice(session: ImportSession, task: ImportTask): """Apply the task's choice to the Album or Item it contains and add it to the library. """ @@ -335,11 +335,11 @@ def apply_choice(session: ImportSession, task: ImportTask): task.set_fields(session.lib) -def resolve_duplicates(session: ImportSession, task: ImportTask): +def _resolve_duplicates(session: ImportSession, task: ImportTask): """Check if a task conflicts with items or albums already imported and ask the session to resolve this. """ - if task.choice_flag in (action.ASIS, action.APPLY, action.RETAG): + if task.choice_flag in (Action.ASIS, Action.APPLY, Action.RETAG): found_duplicates = task.find_duplicates(session.lib) if found_duplicates: log.debug( @@ -360,7 +360,7 @@ def resolve_duplicates(session: ImportSession, task: ImportTask): if duplicate_action == "s": # Skip new. - task.set_choice(action.SKIP) + task.set_choice(Action.SKIP) elif duplicate_action == "k": # Keep both. Do nothing; leave the choice intact. pass diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py index 2d3dc44e8..4ca5cf89f 100644 --- a/beets/importer/tasks.py +++ b/beets/importer/tasks.py @@ -31,7 +31,7 @@ from beets import autotag, config, dbcore, library, plugins, util from .state import ImportState if TYPE_CHECKING: - from .session import ImportSession + from .session import ImportSession, PathBytes # Global logger. log = logging.getLogger("beets") @@ -62,18 +62,26 @@ REIMPORT_FRESH_FIELDS_ITEM = list(REIMPORT_FRESH_FIELDS_ALBUM) log = logging.getLogger("beets") -action = Enum("action", ["SKIP", "ASIS", "TRACKS", "APPLY", "ALBUMS", "RETAG"]) -# The RETAG action represents "don't apply any match, but do record -# new metadata". It's not reachable via the standard command prompt but -# can be used by plugins. - - class ImportAbortError(Exception): """Raised when the user aborts the tagging operation.""" pass +class Action(Enum): + """Enumeration of possible actions for an import task.""" + + SKIP = "SKIP" + ASIS = "ASIS" + TRACKS = "TRACKS" + APPLY = "APPLY" + ALBUMS = "ALBUMS" + RETAG = "RETAG" + # The RETAG action represents "don't apply any match, but do record + # new metadata". It's not reachable via the standard command prompt but + # can be used by plugins. + + class BaseImportTask: """An abstract base class for importer tasks. @@ -143,7 +151,7 @@ class ImportTask(BaseImportTask): system. """ - choice_flag: action | None = None + choice_flag: Action | None = None match: autotag.AlbumMatch | autotag.TrackMatch | None = None # Keep track of the current task item @@ -165,7 +173,7 @@ class ImportTask(BaseImportTask): self.search_ids = [] # user-supplied candidate IDs. def set_choice( - self, choice: action | autotag.AlbumMatch | autotag.TrackMatch + self, choice: Action | autotag.AlbumMatch | autotag.TrackMatch ): """Given an AlbumMatch or TrackMatch object or an action constant, indicates that an action has been selected for this task. @@ -174,20 +182,20 @@ class ImportTask(BaseImportTask): use isinstance to check for them. """ # Not part of the task structure: - assert choice != action.APPLY # Only used internally. + assert choice != Action.APPLY # Only used internally. if choice in ( - action.SKIP, - action.ASIS, - action.TRACKS, - action.ALBUMS, - action.RETAG, + Action.SKIP, + Action.ASIS, + Action.TRACKS, + Action.ALBUMS, + Action.RETAG, ): # TODO: redesign to stricten the type self.choice_flag = choice # type: ignore[assignment] self.match = None else: - self.choice_flag = action.APPLY # Implicit choice. + self.choice_flag = Action.APPLY # Implicit choice. self.match = choice # type: ignore[assignment] def save_progress(self): @@ -205,11 +213,11 @@ class ImportTask(BaseImportTask): @property def apply(self): - return self.choice_flag == action.APPLY + return self.choice_flag == Action.APPLY @property def skip(self): - return self.choice_flag == action.SKIP + return self.choice_flag == Action.SKIP # Convenient data. @@ -219,10 +227,10 @@ class ImportTask(BaseImportTask): (in which case the data comes from the files' current metadata) or APPLY (in which case the data comes from the choice). """ - if self.choice_flag in (action.ASIS, action.RETAG): + if self.choice_flag in (Action.ASIS, Action.RETAG): likelies, consensus = autotag.current_metadata(self.items) return likelies - elif self.choice_flag is action.APPLY and self.match: + elif self.choice_flag is Action.APPLY and self.match: return self.match.info.copy() assert False @@ -232,9 +240,9 @@ class ImportTask(BaseImportTask): If the tasks applies an album match the method only returns the matched items. """ - if self.choice_flag in (action.ASIS, action.RETAG): + if self.choice_flag in (Action.ASIS, Action.RETAG): return list(self.items) - elif self.choice_flag == action.APPLY and isinstance( + elif self.choice_flag == Action.APPLY and isinstance( self.match, autotag.AlbumMatch ): return list(self.match.mapping.keys()) @@ -401,7 +409,7 @@ class ImportTask(BaseImportTask): """ changes = {} - if self.choice_flag == action.ASIS: + if self.choice_flag == Action.ASIS: # Taking metadata "as-is". Guess whether this album is VA. plur_albumartist, freq = util.plurality( [i.albumartist or i.artist for i in self.items] @@ -418,7 +426,7 @@ class ImportTask(BaseImportTask): changes["albumartist"] = config["va_name"].as_str() changes["comp"] = True - elif self.choice_flag in (action.APPLY, action.RETAG): + elif self.choice_flag in (Action.APPLY, Action.RETAG): # Applying autotagged metadata. Just get AA from the first # item. if not self.items[0].albumartist: @@ -473,7 +481,7 @@ class ImportTask(BaseImportTask): # old paths. item.move(operation) - if write and (self.apply or self.choice_flag == action.RETAG): + if write and (self.apply or self.choice_flag == Action.RETAG): item.try_write() with session.lib.transaction(): @@ -490,7 +498,7 @@ class ImportTask(BaseImportTask): self.remove_replaced(lib) self.album = lib.add_album(self.imported_items()) - if self.choice_flag == action.APPLY and isinstance( + if self.choice_flag == Action.APPLY and isinstance( self.match, autotag.AlbumMatch ): # Copy album flexible fields to the DB @@ -672,10 +680,10 @@ class SingletonImportTask(ImportTask): (in which case the data comes from the files' current metadata) or APPLY (in which case the data comes from the choice). """ - assert self.choice_flag in (action.ASIS, action.RETAG, action.APPLY) - if self.choice_flag in (action.ASIS, action.RETAG): + assert self.choice_flag in (Action.ASIS, Action.RETAG, Action.APPLY) + if self.choice_flag in (Action.ASIS, Action.RETAG): return dict(self.item) - elif self.choice_flag is action.APPLY: + elif self.choice_flag is Action.APPLY: return self.match.info.copy() def imported_items(self): diff --git a/beets/test/helper.py b/beets/test/helper.py index 85ea6bcf7..67ae1cfcf 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -658,9 +658,9 @@ class ImportSessionFixture(ImportSession): >>> lib = Library(':memory:') >>> importer = ImportSessionFixture(lib, paths=['/path/to/import']) - >>> importer.add_choice(importer.action.SKIP) - >>> importer.add_choice(importer.action.ASIS) - >>> importer.default_choice = importer.action.APPLY + >>> importer.add_choice(importer.Action.SKIP) + >>> importer.add_choice(importer.Action.ASIS) + >>> importer.default_choice = importer.Action.APPLY >>> importer.run() This imports ``/path/to/import`` into `lib`. It skips the first @@ -673,7 +673,7 @@ class ImportSessionFixture(ImportSession): self._choices = [] self._resolutions = [] - default_choice = importer.action.APPLY + default_choice = importer.Action.APPLY def add_choice(self, choice): self._choices.append(choice) @@ -687,7 +687,7 @@ class ImportSessionFixture(ImportSession): except IndexError: choice = self.default_choice - if choice == importer.action.APPLY: + if choice == importer.Action.APPLY: return task.candidates[0] elif isinstance(choice, int): return task.candidates[choice - 1] @@ -707,7 +707,7 @@ class ImportSessionFixture(ImportSession): res = self.default_resolution if res == self.Resolution.SKIP: - task.set_choice(importer.action.SKIP) + task.set_choice(importer.Action.SKIP) elif res == self.Resolution.REMOVE: task.should_remove_duplicates = True elif res == self.Resolution.MERGE: @@ -720,7 +720,7 @@ class TerminalImportSessionFixture(TerminalImportSession): super().__init__(*args, **kwargs) self._choices = [] - default_choice = importer.action.APPLY + default_choice = importer.Action.APPLY def add_choice(self, choice): self._choices.append(choice) @@ -742,15 +742,15 @@ class TerminalImportSessionFixture(TerminalImportSession): except IndexError: choice = self.default_choice - if choice == importer.action.APPLY: + if choice == importer.Action.APPLY: self.io.addinput("A") - elif choice == importer.action.ASIS: + elif choice == importer.Action.ASIS: self.io.addinput("U") - elif choice == importer.action.ALBUMS: + elif choice == importer.Action.ALBUMS: self.io.addinput("G") - elif choice == importer.action.TRACKS: + elif choice == importer.Action.TRACKS: self.io.addinput("T") - elif choice == importer.action.SKIP: + elif choice == importer.Action.SKIP: self.io.addinput("S") else: self.io.addinput("M") diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 12ea4c94d..7b7554546 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -811,12 +811,12 @@ def _summary_judgment(rec): if config["import"]["quiet"]: if rec == Recommendation.strong: - return importer.action.APPLY + return importer.Action.APPLY else: action = config["import"]["quiet_fallback"].as_choice( { - "skip": importer.action.SKIP, - "asis": importer.action.ASIS, + "skip": importer.Action.SKIP, + "asis": importer.Action.ASIS, } ) elif config["import"]["timid"]: @@ -824,17 +824,17 @@ def _summary_judgment(rec): elif rec == Recommendation.none: action = config["import"]["none_rec_action"].as_choice( { - "skip": importer.action.SKIP, - "asis": importer.action.ASIS, + "skip": importer.Action.SKIP, + "asis": importer.Action.ASIS, "ask": None, } ) else: return None - if action == importer.action.SKIP: + if action == importer.Action.SKIP: print_("Skipping.") - elif action == importer.action.ASIS: + elif action == importer.Action.ASIS: print_("Importing as-is.") return action @@ -1064,7 +1064,7 @@ class TerminalImportSession(importer.ImportSession): # Take immediate action if appropriate. action = _summary_judgment(task.rec) - if action == importer.action.APPLY: + if action == importer.Action.APPLY: match = task.candidates[0] show_change(task.cur_artist, task.cur_album, match) return match @@ -1074,7 +1074,7 @@ class TerminalImportSession(importer.ImportSession): # Loop until we have a choice. while True: # Ask for a choice from the user. The result of - # `choose_candidate` may be an `importer.action`, an + # `choose_candidate` may be an `importer.Action`, an # `AlbumMatch` object for a specific selection, or a # `PromptChoice`. choices = self._get_choices(task) @@ -1089,7 +1089,7 @@ class TerminalImportSession(importer.ImportSession): ) # Basic choices that require no more action here. - if choice in (importer.action.SKIP, importer.action.ASIS): + if choice in (importer.Action.SKIP, importer.Action.ASIS): # Pass selection to main control flow. return choice @@ -1097,7 +1097,7 @@ class TerminalImportSession(importer.ImportSession): # function. elif choice in choices: post_choice = choice.callback(self, task) - if isinstance(post_choice, importer.action): + if isinstance(post_choice, importer.Action): return post_choice elif isinstance(post_choice, autotag.Proposal): # Use the new candidates and continue around the loop. @@ -1121,7 +1121,7 @@ class TerminalImportSession(importer.ImportSession): # Take immediate action if appropriate. action = _summary_judgment(task.rec) - if action == importer.action.APPLY: + if action == importer.Action.APPLY: match = candidates[0] show_item_change(task.item, match) return match @@ -1135,12 +1135,12 @@ class TerminalImportSession(importer.ImportSession): candidates, True, rec, item=task.item, choices=choices ) - if choice in (importer.action.SKIP, importer.action.ASIS): + if choice in (importer.Action.SKIP, importer.Action.ASIS): return choice elif choice in choices: post_choice = choice.callback(self, task) - if isinstance(post_choice, importer.action): + if isinstance(post_choice, importer.Action): return post_choice elif isinstance(post_choice, autotag.Proposal): candidates = post_choice.candidates @@ -1203,7 +1203,7 @@ class TerminalImportSession(importer.ImportSession): if sel == "s": # Skip new. - task.set_choice(importer.action.SKIP) + task.set_choice(importer.Action.SKIP) elif sel == "k": # Keep both. Do nothing; leave the choice intact. pass @@ -1239,16 +1239,16 @@ class TerminalImportSession(importer.ImportSession): """ # Standard, built-in choices. choices = [ - PromptChoice("s", "Skip", lambda s, t: importer.action.SKIP), - PromptChoice("u", "Use as-is", lambda s, t: importer.action.ASIS), + PromptChoice("s", "Skip", lambda s, t: importer.Action.SKIP), + PromptChoice("u", "Use as-is", lambda s, t: importer.Action.ASIS), ] if task.is_album: choices += [ PromptChoice( - "t", "as Tracks", lambda s, t: importer.action.TRACKS + "t", "as Tracks", lambda s, t: importer.Action.TRACKS ), PromptChoice( - "g", "Group albums", lambda s, t: importer.action.ALBUMS + "g", "Group albums", lambda s, t: importer.Action.ALBUMS ), ] choices += [ diff --git a/beets/util/__init__.py b/beets/util/__init__.py index e17de1f51..845d967d6 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -68,6 +68,10 @@ BytesOrStr = Union[str, bytes] PathLike = Union[BytesOrStr, Path] Replacements: TypeAlias = "Sequence[tuple[Pattern[str], str]]" +# Here for now to allow for a easy replace later on +# once we can move to a PathLike (mainly used in importer) +PathBytes = bytes + class HumanReadableError(Exception): """An Exception that can include a human-readable error message to diff --git a/beetsplug/badfiles.py b/beetsplug/badfiles.py index f93f03d5e..0903ebabf 100644 --- a/beetsplug/badfiles.py +++ b/beetsplug/badfiles.py @@ -194,7 +194,7 @@ class BadFiles(BeetsPlugin): sel = ui.input_options(["aBort", "skip", "continue"]) if sel == "s": - return importer.action.SKIP + return importer.Action.SKIP elif sel == "c": return None elif sel == "b": diff --git a/beetsplug/edit.py b/beetsplug/edit.py index 51b36bdab..b92c48839 100644 --- a/beetsplug/edit.py +++ b/beetsplug/edit.py @@ -24,7 +24,7 @@ import yaml from beets import plugins, ui, util from beets.dbcore import types -from beets.importer import action +from beets.importer import Action from beets.ui.commands import PromptChoice, _do_query # These "safe" types can avoid the format/parse cycle that most fields go @@ -380,9 +380,9 @@ class EditPlugin(plugins.BeetsPlugin): # Save the new data. if success: - # Return action.RETAG, which makes the importer write the tags + # Return Action.RETAG, which makes the importer write the tags # to the files if needed without re-applying metadata. - return action.RETAG + return Action.RETAG else: # Edit cancelled / no edits made. Revert changes. for obj in task.items: diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index a1bd26055..5451b4dbb 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -1306,12 +1306,12 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): ): # Album already has art (probably a re-import); skip it. return - if task.choice_flag == importer.action.ASIS: + if task.choice_flag == importer.Action.ASIS: # For as-is imports, don't search Web sources for art. local = True elif task.choice_flag in ( - importer.action.APPLY, - importer.action.RETAG, + importer.Action.APPLY, + importer.Action.RETAG, ): # Search everywhere for art. local = False diff --git a/beetsplug/ihate.py b/beetsplug/ihate.py index 35788ea05..d6357294d 100644 --- a/beetsplug/ihate.py +++ b/beetsplug/ihate.py @@ -15,7 +15,7 @@ """Warns you about things you hate (or even blocks import).""" -from beets.importer import action +from beets.importer import Action from beets.library import Album, Item, parse_query_string from beets.plugins import BeetsPlugin @@ -65,11 +65,11 @@ class IHatePlugin(BeetsPlugin): skip_queries = self.config["skip"].as_str_seq() warn_queries = self.config["warn"].as_str_seq() - if task.choice_flag == action.APPLY: + if task.choice_flag == Action.APPLY: if skip_queries or warn_queries: self._log.debug("processing your hate") if self.do_i_hate_this(task, skip_queries): - task.choice_flag = action.SKIP + task.choice_flag = Action.SKIP self._log.info("skipped: {0}", summary(task)) return if self.do_i_hate_this(task, warn_queries): diff --git a/beetsplug/zero.py b/beetsplug/zero.py index 5d1244dec..7ee624ce7 100644 --- a/beetsplug/zero.py +++ b/beetsplug/zero.py @@ -19,7 +19,7 @@ import re import confuse from mediafile import MediaFile -from beets.importer import action +from beets.importer import Action from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs, input_yn @@ -105,7 +105,7 @@ class ZeroPlugin(BeetsPlugin): self.fields_to_progs[field] = [] def import_task_choice_event(self, session, task): - if task.choice_flag == action.ASIS and not self.warned: + if task.choice_flag == Action.ASIS and not self.warned: self._log.warning('cannot zero in "as-is" mode') self.warned = True # TODO request write in as-is mode diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 0ebff3231..2d30f86c9 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -648,6 +648,6 @@ by the choices on the core importer prompt, and hence should not be used: ``a``, ``s``, ``u``, ``t``, ``g``, ``e``, ``i``, ``b``. Additionally, the callback function can optionally specify the next action to -be performed by returning a ``importer.action`` value. It may also return a +be performed by returning a ``importer.Action`` value. It may also return a ``autotag.Proposal`` value to update the set of current proposals to be considered. diff --git a/test/plugins/test_importadded.py b/test/plugins/test_importadded.py index 608afb399..5c26fdca4 100644 --- a/test/plugins/test_importadded.py +++ b/test/plugins/test_importadded.py @@ -59,7 +59,7 @@ class ImportAddedTest(PluginMixin, ImportTestCase): self.matcher = AutotagStub().install() self.matcher.matching = AutotagStub.IDENT self.importer = self.setup_importer() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) def tearDown(self): super().tearDown() diff --git a/test/test_importer.py b/test/test_importer.py index a28b646cf..34dea6df8 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -34,7 +34,7 @@ from mediafile import MediaFile from beets import config, importer, logging, util from beets.autotag import AlbumInfo, AlbumMatch, TrackInfo -from beets.importer import albums_in_dir +from beets.importer.tasks import albums_in_dir from beets.test import _common from beets.test.helper import ( NEEDS_REFLINK, @@ -324,52 +324,52 @@ class ImportSingletonTest(ImportTestCase): def test_apply_asis_adds_track(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.items().get().title == "Tag Track 1" def test_apply_asis_does_not_add_album(self): assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get() is None def test_apply_asis_adds_singleton_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() self.assert_file_in_lib(b"singletons", b"Tag Track 1.mp3") def test_apply_candidate_adds_track(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "Applied Track 1" def test_apply_candidate_does_not_add_album(self): - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.albums().get() is None def test_apply_candidate_adds_singleton_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assert_file_in_lib(b"singletons", b"Applied Track 1.mp3") def test_skip_does_not_add_first_track(self): - self.importer.add_choice(importer.action.SKIP) + self.importer.add_choice(importer.Action.SKIP) self.importer.run() assert self.lib.items().get() is None def test_skip_adds_other_tracks(self): self.prepare_album_for_import(2) - self.importer.add_choice(importer.action.SKIP) - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.SKIP) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert len(self.lib.items()) == 1 @@ -385,8 +385,8 @@ class ImportSingletonTest(ImportTestCase): self.setup_importer() self.importer.paths = import_files - self.importer.add_choice(importer.action.ASIS) - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert len(self.lib.items()) == 2 @@ -406,7 +406,7 @@ class ImportSingletonTest(ImportTestCase): # As-is item import. assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() for item in self.lib.items(): @@ -421,7 +421,7 @@ class ImportSingletonTest(ImportTestCase): # Autotagged. assert self.lib.albums().get() is None self.importer.clear_choices() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() for item in self.lib.items(): @@ -449,41 +449,41 @@ class ImportTest(ImportTestCase): def test_apply_asis_adds_album(self): assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().album == "Tag Album" def test_apply_asis_adds_tracks(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.items().get().title == "Tag Track 1" def test_apply_asis_adds_album_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() self.assert_file_in_lib(b"Tag Artist", b"Tag Album", b"Tag Track 1.mp3") def test_apply_candidate_adds_album(self): assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.albums().get().album == "Applied Album" def test_apply_candidate_adds_tracks(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "Applied Track 1" def test_apply_candidate_adds_album_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assert_file_in_lib( b"Applied Artist", b"Applied Album", b"Applied Track 1.mp3" @@ -496,14 +496,14 @@ class ImportTest(ImportTestCase): mediafile.genre = "Tag Genre" mediafile.save() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().genre == "" def test_apply_from_scratch_keeps_format(self): config["import"]["from_scratch"] = True - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().format == "MP3" @@ -511,7 +511,7 @@ class ImportTest(ImportTestCase): config["import"]["from_scratch"] = True bitrate = 80000 - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().bitrate == bitrate @@ -521,7 +521,7 @@ class ImportTest(ImportTestCase): import_file = os.path.join(self.import_dir, b"album", b"track_1.mp3") self.assertExists(import_file) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assertNotExists(import_file) @@ -531,26 +531,26 @@ class ImportTest(ImportTestCase): import_file = os.path.join(self.import_dir, b"album", b"track_1.mp3") self.assertExists(import_file) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assertNotExists(import_file) def test_skip_does_not_add_track(self): - self.importer.add_choice(importer.action.SKIP) + self.importer.add_choice(importer.Action.SKIP) self.importer.run() assert self.lib.items().get() is None def test_skip_non_album_dirs(self): self.assertIsDir(os.path.join(self.import_dir, b"album")) self.touch(b"cruft", dir=self.import_dir) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert len(self.lib.albums()) == 1 def test_unmatched_tracks_not_added(self): self.prepare_album_for_import(2) self.matcher.matching = self.matcher.MISSING - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert len(self.lib.items()) == 1 @@ -577,7 +577,7 @@ class ImportTest(ImportTestCase): def test_asis_no_data_source(self): assert self.lib.items().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() with pytest.raises(AttributeError): @@ -599,7 +599,7 @@ class ImportTest(ImportTestCase): # As-is album import. assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() for album in self.lib.albums(): @@ -621,7 +621,7 @@ class ImportTest(ImportTestCase): # Autotagged. assert self.lib.albums().get() is None self.importer.clear_choices() - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() for album in self.lib.albums(): @@ -656,9 +656,9 @@ class ImportTracksTest(ImportTestCase): assert self.lib.items().get() is None assert self.lib.albums().get() is None - self.importer.add_choice(importer.action.TRACKS) - self.importer.add_choice(importer.action.APPLY) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.TRACKS) + self.importer.add_choice(importer.Action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "Applied Track 1" assert self.lib.albums().get() is None @@ -666,9 +666,9 @@ class ImportTracksTest(ImportTestCase): def test_apply_tracks_adds_singleton_path(self): self.assert_lib_dir_empty() - self.importer.add_choice(importer.action.TRACKS) - self.importer.add_choice(importer.action.APPLY) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.TRACKS) + self.importer.add_choice(importer.Action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() self.assert_file_in_lib(b"singletons", b"Applied Track 1.mp3") @@ -687,7 +687,7 @@ class ImportCompilationTest(ImportTestCase): self.matcher.restore() def test_asis_homogenous_sets_albumartist(self): - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Tag Artist" for item in self.lib.items(): @@ -699,7 +699,7 @@ class ImportCompilationTest(ImportTestCase): self.import_media[1].artist = "Another Artist" self.import_media[1].save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Various Artists" for item in self.lib.items(): @@ -711,7 +711,7 @@ class ImportCompilationTest(ImportTestCase): self.import_media[1].artist = "Another Artist" self.import_media[1].save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() for item in self.lib.items(): assert item.comp @@ -722,7 +722,7 @@ class ImportCompilationTest(ImportTestCase): self.import_media[1].artist = "Other Artist" self.import_media[1].save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Other Artist" for item in self.lib.items(): @@ -736,7 +736,7 @@ class ImportCompilationTest(ImportTestCase): mediafile.mb_albumartistid = "Album Artist ID" mediafile.save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Album Artist" assert self.lib.albums().get().mb_albumartistid == "Album Artist ID" @@ -755,7 +755,7 @@ class ImportCompilationTest(ImportTestCase): mediafile.mb_albumartistid = "Album Artist ID" mediafile.save() - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ASIS) self.importer.run() assert self.lib.albums().get().albumartist == "Album Artist" assert self.lib.albums().get().albumartists == [ @@ -802,7 +802,7 @@ class ImportExistingTest(ImportTestCase): self.importer.run() assert len(self.lib.items()) == 1 - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() assert len(self.lib.items()) == 1 @@ -810,18 +810,18 @@ class ImportExistingTest(ImportTestCase): self.importer.run() assert len(self.lib.albums()) == 1 - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() assert len(self.lib.albums()) == 1 def test_does_not_duplicate_singleton_track(self): - self.importer.add_choice(importer.action.TRACKS) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.TRACKS) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert len(self.lib.items()) == 1 - self.reimporter.add_choice(importer.action.TRACKS) - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.TRACKS) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() assert len(self.lib.items()) == 1 @@ -831,7 +831,7 @@ class ImportExistingTest(ImportTestCase): medium.title = "New Title" medium.save() - self.reimporter.add_choice(importer.action.ASIS) + self.reimporter.add_choice(importer.Action.ASIS) self.reimporter.run() assert self.lib.items().get().title == "New Title" @@ -846,7 +846,7 @@ class ImportExistingTest(ImportTestCase): ) self.assert_file_in_lib(old_path) - self.reimporter.add_choice(importer.action.ASIS) + self.reimporter.add_choice(importer.Action.ASIS) self.reimporter.run() self.assert_file_in_lib( b"Applied Artist", b"Applied Album", b"New Title.mp3" @@ -865,7 +865,7 @@ class ImportExistingTest(ImportTestCase): self.assert_file_in_lib(old_path) config["import"]["copy"] = False - self.reimporter.add_choice(importer.action.ASIS) + self.reimporter.add_choice(importer.Action.ASIS) self.reimporter.run() self.assert_file_not_in_lib( b"Applied Artist", b"Applied Album", b"New Title.mp3" @@ -880,7 +880,7 @@ class ImportExistingTest(ImportTestCase): ) self.reimporter = self.setup_importer() - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() new_path = os.path.join( b"Applied Artist", b"Applied Album", b"Applied Track 1.mp3" @@ -899,7 +899,7 @@ class ImportExistingTest(ImportTestCase): ) self.reimporter = self.setup_importer(move=True) - self.reimporter.add_choice(importer.action.APPLY) + self.reimporter.add_choice(importer.Action.APPLY) self.reimporter.run() self.assertNotExists(self.import_media[0].path) @@ -913,9 +913,9 @@ class GroupAlbumsImportTest(ImportTestCase): self.setup_importer() # Split tracks into two albums and use both as-is - self.importer.add_choice(importer.action.ALBUMS) - self.importer.add_choice(importer.action.ASIS) - self.importer.add_choice(importer.action.ASIS) + self.importer.add_choice(importer.Action.ALBUMS) + self.importer.add_choice(importer.Action.ASIS) + self.importer.add_choice(importer.Action.ASIS) def tearDown(self): super().tearDown() @@ -972,7 +972,7 @@ class GlobalGroupAlbumsImportTest(GroupAlbumsImportTest): def setUp(self): super().setUp() self.importer.clear_choices() - self.importer.default_choice = importer.action.ASIS + self.importer.default_choice = importer.Action.ASIS config["import"]["group_albums"] = True @@ -1019,7 +1019,7 @@ class InferAlbumDataTest(BeetsTestCase): ) def test_asis_homogenous_single_artist(self): - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() assert not self.items[0].comp assert self.items[0].albumartist == self.items[2].artist @@ -1027,7 +1027,7 @@ class InferAlbumDataTest(BeetsTestCase): def test_asis_heterogenous_va(self): self.items[0].artist = "another artist" self.items[1].artist = "some other artist" - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() @@ -1037,7 +1037,7 @@ class InferAlbumDataTest(BeetsTestCase): def test_asis_comp_applied_to_all_items(self): self.items[0].artist = "another artist" self.items[1].artist = "some other artist" - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() @@ -1047,7 +1047,7 @@ class InferAlbumDataTest(BeetsTestCase): def test_asis_majority_artist_single_artist(self): self.items[0].artist = "another artist" - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() @@ -1060,7 +1060,7 @@ class InferAlbumDataTest(BeetsTestCase): for item in self.items: item.albumartist = "some album artist" item.mb_albumartistid = "some album artist id" - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() @@ -1089,7 +1089,7 @@ class InferAlbumDataTest(BeetsTestCase): def test_small_single_artist_album(self): self.items = [self.items[0]] self.task.items = self.items - self.task.set_choice(importer.action.ASIS) + self.task.set_choice(importer.Action.ASIS) self.task.align_album_level_fields() assert not self.items[0].comp @@ -1599,7 +1599,7 @@ class ReimportTest(ImportTestCase): def _setup_session(self, singletons=False): self.setup_importer(import_dir=self.libdir, singletons=singletons) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) def _album(self): return self.lib.albums().get() @@ -1845,7 +1845,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): search_ids=[self.MB_RELEASE_PREFIX + self.ID_RELEASE_0] ) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.albums().get().album == "VALID_RELEASE_0" @@ -1858,7 +1858,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): ) self.importer.add_choice(2) # Pick the 2nd best match (release 1). - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.albums().get().album == "VALID_RELEASE_1" @@ -1867,7 +1867,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): search_ids=[self.MB_RECORDING_PREFIX + self.ID_RECORDING_0] ) - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "VALID_RECORDING_0" @@ -1880,7 +1880,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): ) self.importer.add_choice(2) # Pick the 2nd best match (recording 1). - self.importer.add_choice(importer.action.APPLY) + self.importer.add_choice(importer.Action.APPLY) self.importer.run() assert self.lib.items().get().title == "VALID_RECORDING_1" diff --git a/test/test_plugins.py b/test/test_plugins.py index d273de698..25f1f3c66 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -24,10 +24,10 @@ from mediafile import MediaFile from beets import config, plugins, ui from beets.dbcore import types from beets.importer import ( + Action, ArchiveImportTask, SentinelImportTask, SingletonImportTask, - action, ) from beets.library import Item from beets.plugins import MetadataSourcePlugin @@ -389,7 +389,7 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): "aBort", ) + ("Foo", "baR") - self.importer.add_choice(action.SKIP) + self.importer.add_choice(Action.SKIP) self.importer.run() self.mock_input_options.assert_called_once_with( opts, default="a", require=ANY @@ -424,7 +424,7 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): ) + ("Foo", "baR") config["import"]["singletons"] = True - self.importer.add_choice(action.SKIP) + self.importer.add_choice(Action.SKIP) self.importer.run() self.mock_input_options.assert_called_with( opts, default="a", require=ANY @@ -461,7 +461,7 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): "enter Id", "aBort", ) + ("baZ",) - self.importer.add_choice(action.SKIP) + self.importer.add_choice(Action.SKIP) self.importer.run() self.mock_input_options.assert_called_once_with( opts, default="a", require=ANY @@ -523,7 +523,7 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): return [ui.commands.PromptChoice("f", "Foo", self.foo)] def foo(self, session, task): - return action.SKIP + return Action.SKIP self.register_plugin(DummyPlugin) # Default options + extra choices by the plugin ('Foo', 'Bar') From ce61ff0006cf0d8e5a4c5c613e212ad6af04eaf7 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr <sebastian@mohrenclan.de> Date: Tue, 13 May 2025 13:05:07 +0200 Subject: [PATCH 041/728] Removed pathbytes (lint error) --- beets/importer/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/importer/tasks.py b/beets/importer/tasks.py index 4ca5cf89f..d2f638c55 100644 --- a/beets/importer/tasks.py +++ b/beets/importer/tasks.py @@ -31,7 +31,7 @@ from beets import autotag, config, dbcore, library, plugins, util from .state import ImportState if TYPE_CHECKING: - from .session import ImportSession, PathBytes + from .session import ImportSession # Global logger. log = logging.getLogger("beets") From b92a1b3d9818a4ae8153a7c3f130cd423079761a Mon Sep 17 00:00:00 2001 From: Gavin Tronset <gavin@joydrive.com> Date: Tue, 13 May 2025 09:48:43 -0700 Subject: [PATCH 042/728] Add `beets-filetote` to community plugins docs (#5779) Add link to community plugin [`beets-filetote`](https://github.com/gtronset/beets-filetote). This plugin is the spiritual successor to [beets-copyartifacts](https://github.com/adammillerio/beets-copyartifacts) (`beets-copyartifacts3` was last updated 3 years ago) and [beets-extrafiles](https://github.com/Holzhaus/beets-extrafiles) (last updated 5 years ago). Given the updates and changes in beets and how outdated those plugins are, does it make sense to keep `beets-copyartifacts` in the community plugins list? --------- Co-authored-by: Sebastian Mohr <39738318+semohr@users.noreply.github.com> --- docs/plugins/index.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index b7998ef19..bd137291e 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -465,6 +465,10 @@ Here are a few of the plugins written by the beets community: `dsedivec`_ Has two plugins: ``edit`` and ``moveall``. +`beets-filetote`_ + Helps bring non-music extra files, attachments, and artifacts during + imports and CLI file manipulation actions (`beet move`, etc.). + `beets-follow`_ Lets you check for new albums from artists you like. @@ -560,6 +564,7 @@ Here are a few of the plugins written by the beets community: .. _cmus: http://cmus.sourceforge.net/ .. _beet-amazon: https://github.com/jmwatte/beet-amazon .. _beets-alternatives: https://github.com/geigerzaehler/beets-alternatives +.. _beets-filetote: https://github.com/gtronset/beets-filetote .. _beets-follow: https://github.com/nolsto/beets-follow .. _beets-ibroadcast: https://github.com/ctrueden/beets-ibroadcast .. _iBroadcast: https://ibroadcast.com/ From 28781e9077ce6b6eb15504c599476d7494790d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 14 May 2025 03:44:18 +0100 Subject: [PATCH 043/728] Pin Poetry version <2 --- CONTRIBUTING.rst | 9 + docs/changelog.rst | 12 +- poetry.lock | 471 +++++++++++++++------------------------------ pyproject.toml | 2 +- 4 files changed, 172 insertions(+), 322 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 9010db2c3..5fccb8e80 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -87,6 +87,15 @@ Install `poetry`_ and `poethepoet`_ using `pipx`_:: $ pipx install poetry poethepoet +.. admonition:: Check ``tool.pipx-install`` section in ``pyproject.toml`` to + see supported versions + + :: + + [tool.pipx-install] + poethepoet = ">=0.26" + poetry = "<2" + .. _pipx: https://pipx.pypa.io/stable .. _pipx-installation-instructions: https://pipx.pypa.io/stable/installation/ diff --git a/docs/changelog.rst b/docs/changelog.rst index ab0b9519d..ee78db648 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,18 +6,16 @@ Changelog goes here! Please add your entry to the bottom of one of the lists bel Unreleased ---------- -New features: - Bug fixes: - * :doc:`/reference/pathformat`: Fixed a regression where path legalization incorrectly removed parts of user-configured path formats that followed a dot (**.**). :bug:`5771` For packagers: - -Other changes: +* Force ``poetry`` version below 2 to avoid it mangling file modification times + in ``sdist`` package. + :bug:`5770` 2.3.0 (May 07, 2025) -------------------- @@ -115,8 +113,8 @@ Other changes: :bug:`5539` * :doc:`/plugins/smartplaylist`: URL-encode additional item `fields` within generated EXTM3U playlists instead of JSON-encoding them. -* typehints: `./beets/importer.py` file now has improved typehints. -* typehints: `./beets/plugins.py` file now includes typehints. +* typehints: `./beets/importer.py` file now has improved typehints. +* typehints: `./beets/plugins.py` file now includes typehints. * :doc:`plugins/ftintitle`: Optimize the plugin by avoiding unnecessary writes to the database. * Database models are now serializable with pickle. diff --git a/poetry.lock b/poetry.lock index fe9dec791..bdd0ee0ca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -6,8 +6,6 @@ version = "0.0.5" description = "A collection of accessible pygments styles" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, @@ -26,8 +24,6 @@ version = "0.7.16" description = "A light, configurable Sphinx theme" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, @@ -39,7 +35,6 @@ version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" -groups = ["main", "test"] files = [ {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, @@ -53,7 +48,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -62,8 +57,6 @@ version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = true python-versions = "*" -groups = ["main"] -markers = "extra == \"sonosupdate\"" files = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -75,8 +68,6 @@ version = "3.0.1" description = "Multi-library, cross-platform audio decoding." optional = true python-versions = ">=3.6" -groups = ["main"] -markers = "extra == \"autobpm\" or extra == \"chroma\"" files = [ {file = "audioread-3.0.1-py3-none-any.whl", hash = "sha256:4cdce70b8adc0da0a3c9e0d85fb10b3ace30fbdf8d1670fd443929b61d117c33"}, {file = "audioread-3.0.1.tar.gz", hash = "sha256:ac5460a5498c48bdf2e8e767402583a4dcd13f4414d286f42ce4379e8b35066d"}, @@ -91,15 +82,13 @@ version = "2.17.0" description = "Internationalization utilities" optional = true python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [package.extras] -dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] +dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] [[package]] name = "beautifulsoup4" @@ -107,7 +96,6 @@ version = "4.13.4" description = "Screen-scraping library" optional = false python-versions = ">=3.7.0" -groups = ["main", "test"] files = [ {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, @@ -130,7 +118,6 @@ version = "1.9.0" description = "Fast, simple object-to-object and broadcast signaling" optional = false python-versions = ">=3.9" -groups = ["main", "test", "typing"] files = [ {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, @@ -142,8 +129,6 @@ version = "1.1.0" description = "Python bindings for the Brotli compression library" optional = false python-versions = "*" -groups = ["main", "test"] -markers = "platform_python_implementation == \"CPython\"" files = [ {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, @@ -278,8 +263,6 @@ version = "1.1.0.0" description = "Python CFFI bindings to the Brotli library" optional = false python-versions = ">=3.7" -groups = ["main", "test"] -markers = "platform_python_implementation == \"PyPy\"" files = [ {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, @@ -319,7 +302,6 @@ version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["main", "test"] files = [ {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, @@ -331,7 +313,6 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -401,7 +382,6 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] -markers = {main = "extra == \"autobpm\" or extra == \"reflink\" or platform_python_implementation == \"PyPy\"", test = "platform_python_implementation == \"PyPy\""} [package.dependencies] pycparser = "*" @@ -412,7 +392,6 @@ version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main", "test"] files = [ {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, @@ -514,7 +493,6 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["main", "release", "test", "typing"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -529,7 +507,6 @@ version = "2.1.13" description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["test"] files = [ {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"}, {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"}, @@ -545,8 +522,6 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "release", "test", "typing"] -markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -558,7 +533,6 @@ version = "2.0.1" description = "Painless YAML configuration." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "confuse-2.0.1-py3-none-any.whl", hash = "sha256:9b9e5bbc70e2cb9b318bcab14d917ec88e21bf1b724365e3815eb16e37aabd2a"}, {file = "confuse-2.0.1.tar.gz", hash = "sha256:7379a2ad49aaa862b79600cc070260c1b7974d349f4fa5e01f9afa6c4dd0611f"}, @@ -573,7 +547,6 @@ version = "7.8.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" -groups = ["test"] files = [ {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, @@ -644,7 +617,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +toml = ["tomli"] [[package]] name = "dbus-python" @@ -652,8 +625,6 @@ version = "1.4.0" description = "Python bindings for libdbus" optional = true python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"metasync\"" files = [ {file = "dbus-python-1.4.0.tar.gz", hash = "sha256:991666e498f60dbf3e49b8b7678f5559b8a65034fdf61aae62cdecdb7d89c770"}, ] @@ -668,8 +639,6 @@ version = "5.2.1" description = "Decorators for Humans" optional = true python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, @@ -681,8 +650,6 @@ version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, @@ -690,17 +657,18 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["main", "test"] -markers = "python_version < \"3.11\"" files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] @@ -710,7 +678,6 @@ version = "1.2.0" description = "Infer file type and MIME type of any file/buffer. No external dependencies." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"}, {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, @@ -718,23 +685,23 @@ files = [ [[package]] name = "flask" -version = "3.1.0" +version = "3.1.1" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.9" -groups = ["main", "test", "typing"] files = [ - {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, - {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, + {file = "flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c"}, + {file = "flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e"}, ] [package.dependencies] -blinker = ">=1.9" +blinker = ">=1.9.0" click = ">=8.1.3" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.2" -Jinja2 = ">=3.1.2" -Werkzeug = ">=3.1" +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" [package.extras] async = ["asgiref (>=3.2)"] @@ -746,8 +713,6 @@ version = "5.0.1" description = "A Flask extension simplifying CORS support" optional = true python-versions = "<4.0,>=3.9" -groups = ["main"] -markers = "extra == \"aura\" or extra == \"web\"" files = [ {file = "flask_cors-5.0.1-py3-none-any.whl", hash = "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c"}, {file = "flask_cors-5.0.1.tar.gz", hash = "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c"}, @@ -763,7 +728,6 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -775,7 +739,6 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -797,7 +760,6 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -810,7 +772,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -822,7 +784,6 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main", "test"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -837,8 +798,6 @@ version = "0.2.0" description = "Cross-platform network interface and IP address enumeration library" optional = true python-versions = "*" -groups = ["main"] -markers = "extra == \"sonosupdate\"" files = [ {file = "ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748"}, {file = "ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4"}, @@ -850,8 +809,6 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -863,8 +820,6 @@ version = "8.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" -groups = ["main", "test", "typing"] -markers = "python_version == \"3.9\"" files = [ {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, @@ -874,12 +829,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -888,7 +843,6 @@ version = "1.0.1" description = "deflate64 compression/decompression library" optional = false python-versions = ">=3.9" -groups = ["main", "test"] files = [ {file = "inflate64-1.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5122a188995e47a735ab969edc9129d42bbd97b993df5a3f0819b87205ce81b4"}, {file = "inflate64-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:975ed694c680e46a5c0bb872380a9c9da271a91f9c0646561c58e8f3714347d4"}, @@ -944,7 +898,6 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -956,7 +909,6 @@ version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = false python-versions = ">=3.8" -groups = ["main", "test", "typing"] files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -968,7 +920,6 @@ version = "1.2.0" description = "Approximate and phonetic matching of strings." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "jellyfish-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:50b6d2a123d3e0929cf136c6c26a6774338be7eb9d140a94223f56e3339b2f80"}, {file = "jellyfish-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:baa1e44244cba9da6d67a50d3076dd7567e3b91caa9887bb68e20f321e0d2500"}, @@ -1053,7 +1004,6 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["main", "test", "typing"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -1071,8 +1021,6 @@ version = "1.5.0" description = "Lightweight pipelining with Python functions" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491"}, {file = "joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5"}, @@ -1084,8 +1032,6 @@ version = "1.0.9" description = "Language detection library ported from Google's language-detection." optional = true python-versions = "*" -groups = ["main"] -markers = "extra == \"fetchart\" or extra == \"lyrics\"" files = [ {file = "langdetect-1.0.9-py2-none-any.whl", hash = "sha256:7cbc0746252f19e76f77c0b1690aadf01963be835ef0cd4b56dddf2a8f1dfc2a"}, {file = "langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0"}, @@ -1100,7 +1046,6 @@ version = "0.5.12" description = "Linear Assignment Problem solver (LAPJV/LAPMOD)." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "lap-0.5.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c3a38070b24531949e30d7ebc83ca533fcbef6b1d6562f035cae3b44dfbd5ec"}, {file = "lap-0.5.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a301dc9b8a30e41e4121635a0e3d0f6374a08bb9509f618d900e18d209b815c4"}, @@ -1167,8 +1112,6 @@ version = "0.4" description = "Makes it easy to load subpackages and functions on demand." optional = true python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc"}, {file = "lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1"}, @@ -1188,8 +1131,6 @@ version = "0.10.2.post1" description = "Python module for audio and music processing" optional = true python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "librosa-0.10.2.post1-py3-none-any.whl", hash = "sha256:dc882750e8b577a63039f25661b7e39ec4cfbacc99c1cffba666cd664fb0a7a0"}, {file = "librosa-0.10.2.post1.tar.gz", hash = "sha256:cd99f16717cbcd1e0983e37308d1db46a6f7dfc2e396e5a9e61e6821e44bd2e7"}, @@ -1221,8 +1162,6 @@ version = "0.43.0" description = "lightweight wrapper around basic LLVM functionality" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761"}, {file = "llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc"}, @@ -1253,8 +1192,6 @@ version = "5.4.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = true python-versions = ">=3.6" -groups = ["main"] -markers = "extra == \"sonosupdate\"" files = [ {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c"}, {file = "lxml-5.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7"}, @@ -1400,7 +1337,6 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["main", "test", "typing"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1471,7 +1407,6 @@ version = "0.13.0" description = "Handles low-level interfacing for files' tags. Wraps Mutagen to" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "mediafile-0.13.0-py3-none-any.whl", hash = "sha256:cd8d183d0e0671b5203a86e92cf4e3338ecc892a1ec9dcd7ec0ed87779e514cb"}, {file = "mediafile-0.13.0.tar.gz", hash = "sha256:de71063e1bffe9733d6ccad526ea7dac8a9ce760105827f81ab0cb034c729a6d"}, @@ -1490,7 +1425,6 @@ version = "5.2.0" description = "Rolling backport of unittest.mock for all Pythons" optional = false python-versions = ">=3.6" -groups = ["test"] files = [ {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}, {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}, @@ -1507,8 +1441,6 @@ version = "1.1.0" description = "MessagePack serializer" optional = true python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, @@ -1582,7 +1514,6 @@ version = "0.2.3" description = "multi volume file wrapper library" optional = false python-versions = ">=3.6" -groups = ["main", "test"] files = [ {file = "multivolumefile-0.2.3-py3-none-any.whl", hash = "sha256:237f4353b60af1703087cf7725755a1f6fcaeeea48421e1896940cd1c920d678"}, {file = "multivolumefile-0.2.3.tar.gz", hash = "sha256:a0648d0aafbc96e59198d5c17e9acad7eb531abea51035d08ce8060dcad709d6"}, @@ -1599,7 +1530,6 @@ version = "0.7.1" description = "Python bindings for the MusicBrainz NGS and the Cover Art Archive webservices" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] files = [ {file = "musicbrainzngs-0.7.1-py2.py3-none-any.whl", hash = "sha256:e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10"}, {file = "musicbrainzngs-0.7.1.tar.gz", hash = "sha256:ab1c0100fd0b305852e65f2ed4113c6de12e68afd55186987b8ed97e0f98e627"}, @@ -1611,7 +1541,6 @@ version = "1.47.0" description = "read and write audio tags for many formats" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"}, {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, @@ -1623,7 +1552,6 @@ version = "1.15.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" -groups = ["typing"] files = [ {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, @@ -1677,7 +1605,6 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -1689,8 +1616,6 @@ version = "0.60.0" description = "compiling Python code using LLVM" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651"}, {file = "numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b"}, @@ -1725,7 +1650,6 @@ version = "2.0.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, @@ -1780,7 +1704,6 @@ version = "3.2.2" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false python-versions = ">=3.6" -groups = ["main", "test"] files = [ {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, @@ -1797,7 +1720,6 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "release", "test"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -1809,8 +1731,6 @@ version = "11.2.1" description = "Python Imaging Library (Fork)" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"aura\" or extra == \"embedart\" or extra == \"fetchart\" or extra == \"thumbnails\"" files = [ {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"}, {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"}, @@ -1901,19 +1821,18 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions ; python_version < \"3.10\""] +typing = ["typing-extensions"] xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.3.7" +version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ - {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, - {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] [package.extras] @@ -1927,7 +1846,6 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1943,8 +1861,6 @@ version = "1.8.2" description = "A friend to fetch your data files" optional = true python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47"}, {file = "pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10"}, @@ -1966,8 +1882,6 @@ version = "7.0.0" description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." optional = false python-versions = ">=3.6" -groups = ["main", "test"] -markers = "sys_platform != \"cygwin\"" files = [ {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, @@ -1991,7 +1905,6 @@ version = "0.22.0" description = "Pure python 7-zip library" optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "py7zr-0.22.0-py3-none-any.whl", hash = "sha256:993b951b313500697d71113da2681386589b7b74f12e48ba13cc12beca79d078"}, {file = "py7zr-0.22.0.tar.gz", hash = "sha256:c6c7aea5913535184003b73938490f9a4d8418598e533f9ca991d3b8e45a139e"}, @@ -2022,8 +1935,6 @@ version = "1.3.0" description = "bindings for Chromaprint acoustic fingerprinting and the Acoustid API" optional = true python-versions = "*" -groups = ["main"] -markers = "extra == \"chroma\"" files = [ {file = "pyacoustid-1.3.0.tar.gz", hash = "sha256:5f4f487191c19ebb908270b1b7b5297f132da332b1568b96a914574c079ed177"}, ] @@ -2038,7 +1949,6 @@ version = "1.0.6" description = "bcj filter library" optional = false python-versions = ">=3.9" -groups = ["main", "test"] files = [ {file = "pybcj-1.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0fc8eda59e9e52d807f411de6db30aadd7603aa0cb0a830f6f45226b74be1926"}, {file = "pybcj-1.0.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0495443e8691510129f0c589ed956af4962c22b7963c5730b0c80c9c5b818c06"}, @@ -2098,8 +2008,6 @@ version = "1.28.0" description = "Python interface for cairo" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"bpd\" or extra == \"replaygain\"" files = [ {file = "pycairo-1.28.0-cp310-cp310-win32.whl", hash = "sha256:53e6dbc98456f789965dad49ef89ce2c62f9a10fc96c8d084e14da0ffb73d8a6"}, {file = "pycairo-1.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:c8ab91a75025f984bc327ada335c787efb61c929ea0512063793cb36cee503d4"}, @@ -2125,12 +2033,10 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -markers = {main = "extra == \"autobpm\" or extra == \"reflink\" or platform_python_implementation == \"PyPy\"", test = "platform_python_implementation == \"PyPy\""} [[package]] name = "pycryptodomex" @@ -2138,7 +2044,6 @@ version = "3.22.0" description = "Cryptographic library for Python" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "test"] files = [ {file = "pycryptodomex-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:41673e5cc39a8524557a0472077635d981172182c9fe39ce0b5f5c19381ffaff"}, {file = "pycryptodomex-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:276be1ed006e8fd01bba00d9bd9b60a0151e478033e86ea1cb37447bbc057edc"}, @@ -2177,8 +2082,6 @@ version = "0.16.1" description = "Bootstrap-based Sphinx theme from the PyData community" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "pydata_sphinx_theme-0.16.1-py3-none-any.whl", hash = "sha256:225331e8ac4b32682c18fcac5a57a6f717c4e632cea5dd0e247b55155faeccde"}, {file = "pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7"}, @@ -2206,8 +2109,6 @@ version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = true python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -2222,8 +2123,6 @@ version = "3.52.3" description = "Python bindings for GObject Introspection" optional = true python-versions = "<4.0,>=3.9" -groups = ["main"] -markers = "extra == \"bpd\" or extra == \"replaygain\"" files = [ {file = "pygobject-3.52.3.tar.gz", hash = "sha256:00e427d291e957462a8fad659a9f9c8be776ff82a8b76bdf402f1eaeec086d82"}, ] @@ -2237,7 +2136,6 @@ version = "5.5.0" description = "A Python interface to Last.fm and Libre.fm" optional = false python-versions = ">=3.9" -groups = ["main", "test"] files = [ {file = "pylast-5.5.0-py3-none-any.whl", hash = "sha256:a28b5dbf69ef71b868e42ce27c74e4feea5277fbee26960549604ce34d631bbe"}, {file = "pylast-5.5.0.tar.gz", hash = "sha256:b6e95cf11fb99779cd451afd5dd68c4036c44f88733cf2346ba27317c1869da4"}, @@ -2255,7 +2153,6 @@ version = "1.1.1" description = "PPMd compression/decompression library" optional = false python-versions = ">=3.9" -groups = ["main", "test"] files = [ {file = "pyppmd-1.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:406b184132c69e3f60ea9621b69eaa0c5494e83f82c307b3acce7b86a4f8f888"}, {file = "pyppmd-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2cf003bb184adf306e1ac1828107307927737dde63474715ba16462e266cbef"}, @@ -2326,7 +2223,6 @@ version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, @@ -2349,7 +2245,6 @@ version = "6.1.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" -groups = ["test"] files = [ {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, @@ -2368,7 +2263,6 @@ version = "1.3.0" description = "A set of py.test fixtures to test Flask applications." optional = false python-versions = ">=3.7" -groups = ["test"] files = [ {file = "pytest-flask-1.3.0.tar.gz", hash = "sha256:58be1c97b21ba3c4d47e0a7691eb41007748506c36bf51004f78df10691fa95e"}, {file = "pytest_flask-1.3.0-py3-none-any.whl", hash = "sha256:c0e36e6b0fddc3b91c4362661db83fa694d1feb91fa505475be6732b5bc8c253"}, @@ -2388,7 +2282,6 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "test"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2403,7 +2296,6 @@ version = "3.1.1" description = "A Python MPD client library" optional = false python-versions = ">=3.6" -groups = ["main", "test"] files = [ {file = "python-mpd2-3.1.1.tar.gz", hash = "sha256:4baec3584cc43ed9948d5559079fafc2679b06b2ade273e909b3582654b2b3f5"}, {file = "python_mpd2-3.1.1-py2.py3-none-any.whl", hash = "sha256:86bf1100a0b135959d74a9a7a58cf0515bf30bb54eb25ae6fb8e175e50300fc3"}, @@ -2418,7 +2310,6 @@ version = "2.8" description = "Python API client for Discogs" optional = false python-versions = "*" -groups = ["main", "test"] files = [ {file = "python3_discogs_client-2.8-py3-none-any.whl", hash = "sha256:60d63a613da73afeb818015e680fa5f007ffaa94d97578070e7ee4f11dc1b1b3"}, {file = "python3_discogs_client-2.8.tar.gz", hash = "sha256:0f2c77f4ff491a6ef60fe892032028df899808e65efcd48249b4ecf21146b33b"}, @@ -2438,7 +2329,6 @@ version = "0.28" description = "PyXDG contains implementations of freedesktop.org standards in python." optional = false python-versions = "*" -groups = ["main", "test"] files = [ {file = "pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"}, {file = "pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4"}, @@ -2450,7 +2340,6 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -2509,104 +2398,116 @@ files = [ [[package]] name = "pyzstd" -version = "0.16.2" +version = "0.17.0" description = "Python bindings to Zstandard (zstd) compression library." optional = false python-versions = ">=3.5" -groups = ["main", "test"] files = [ - {file = "pyzstd-0.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:637376c8f8cbd0afe1cab613f8c75fd502bd1016bf79d10760a2d5a00905fe62"}, - {file = "pyzstd-0.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e7a7118cbcfa90ca2ddbf9890c7cb582052a9a8cf2b7e2c1bbaf544bee0f16a"}, - {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a74cb1ba05876179525144511eed3bd5a509b0ab2b10632c1215a85db0834dfd"}, - {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c084dde218ffbf112e507e72cbf626b8f58ce9eb23eec129809e31037984662"}, - {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4646459ebd3d7a59ddbe9312f020bcf7cdd1f059a2ea07051258f7af87a0b31"}, - {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14bfc2833cc16d7657fc93259edeeaa793286e5031b86ca5dc861ba49b435fce"}, - {file = "pyzstd-0.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f27d488f19e5bf27d1e8aa1ae72c6c0a910f1e1ffbdf3c763d02ab781295dd27"}, - {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91e134ca968ff7dcfa8b7d433318f01d309b74ee87e0d2bcadc117c08e1c80db"}, - {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6b5f64cd3963c58b8f886eb6139bb8d164b42a74f8a1bb95d49b4804f4592d61"}, - {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b4a8266871b9e0407f9fd8e8d077c3558cf124d174e6357b523d14f76971009"}, - {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1bb19f7acac30727354c25125922aa59f44d82e0e6a751df17d0d93ff6a73853"}, - {file = "pyzstd-0.16.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3008325b7368e794d66d4d98f2ee1d867ef5afd09fd388646ae02b25343c420d"}, - {file = "pyzstd-0.16.2-cp310-cp310-win32.whl", hash = "sha256:66f2d5c0bbf5bf32c577aa006197b3525b80b59804450e2c32fbcc2d16e850fd"}, - {file = "pyzstd-0.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fe5f5459ebe1161095baa7a86d04ab625b35148f6c425df0347ed6c90a2fd58"}, - {file = "pyzstd-0.16.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1bdbe7f01c7f37d5cd07be70e32a84010d7dfd6677920c0de04cf7d245b60d"}, - {file = "pyzstd-0.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1882a3ceaaf9adc12212d587d150ec5e58cfa9a765463d803d739abbd3ac0f7a"}, - {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea46a8b9d60f6a6eba29facba54c0f0d70328586f7ef0da6f57edf7e43db0303"}, - {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7865bc06589cdcecdede0deefe3da07809d5b7ad9044c224d7b2a0867256957"}, - {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52f938a65b409c02eb825e8c77fc5ea54508b8fc44b5ce226db03011691ae8cc"}, - {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97620d3f53a0282947304189deef7ca7f7d0d6dfe15033469dc1c33e779d5e5"}, - {file = "pyzstd-0.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c40e9983d017108670dc8df68ceef14c7c1cf2d19239213274783041d0e64c"}, - {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4b3b2c6161066e4bde6af1cf78ed3acf5d731884dd13fdf31f1db10830080"}, - {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:454f31fd84175bb203c8c424f2255a343fa9bd103461a38d1bf50487c3b89508"}, - {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5ef754a93743f08fb0386ce3596780bfba829311b49c8f4107af1a4bcc16935d"}, - {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:be81081db9166e10846934f0e3576a263cbe18d81eca06e6a5c23533f8ce0dc6"}, - {file = "pyzstd-0.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:738bcb2fa1e5f1868986f5030955e64de53157fa1141d01f3a4daf07a1aaf644"}, - {file = "pyzstd-0.16.2-cp311-cp311-win32.whl", hash = "sha256:0ea214c9b97046867d1657d55979021028d583704b30c481a9c165191b08d707"}, - {file = "pyzstd-0.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:c17c0fc02f0e75b0c7cd21f8eaf4c6ce4112333b447d93da1773a5f705b2c178"}, - {file = "pyzstd-0.16.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4081fd841a9efe9ded7290ee7502dbf042c4158b90edfadea3b8a072c8ec4e1"}, - {file = "pyzstd-0.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd3fa45d2aeb65367dd702806b2e779d13f1a3fa2d13d5ec777cfd09de6822de"}, - {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8b5f0d2c07994a5180d8259d51df6227a57098774bb0618423d7eb4a7303467"}, - {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60c9d25b15c7ae06ed5d516d096a0d8254f9bed4368b370a09cccf191eaab5cb"}, - {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29acf31ce37254f6cad08deb24b9d9ba954f426fa08f8fae4ab4fdc51a03f4ae"}, - {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec77612a17697a9f7cf6634ffcee616eba9b997712fdd896e77fd19ab3a0618"}, - {file = "pyzstd-0.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:313ea4974be93be12c9a640ab40f0fc50a023178aae004a8901507b74f190173"}, - {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e91acdefc8c2c6c3b8d5b1b5fe837dce4e591ecb7c0a2a50186f552e57d11203"}, - {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:929bd91a403539e72b5b5cb97f725ac4acafe692ccf52f075e20cd9bf6e5493d"}, - {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:740837a379aa32d110911ebcbbc524f9a9b145355737527543a884bd8777ca4f"}, - {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:adfc0e80dd157e6d1e0b0112c8ecc4b58a7a23760bd9623d74122ef637cfbdb6"}, - {file = "pyzstd-0.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:79b183beae1c080ad3dca39019e49b7785391947f9aab68893ad85d27828c6e7"}, - {file = "pyzstd-0.16.2-cp312-cp312-win32.whl", hash = "sha256:b8d00631a3c466bc313847fab2a01f6b73b3165de0886fb03210e08567ae3a89"}, - {file = "pyzstd-0.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:c0d43764e9a60607f35d8cb3e60df772a678935ab0e02e2804d4147377f4942c"}, - {file = "pyzstd-0.16.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3ae9ae7ad730562810912d7ecaf1fff5eaf4c726f4b4dfe04784ed5f06d7b91f"}, - {file = "pyzstd-0.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2ce8d3c213f76a564420f3d0137066ac007ce9fb4e156b989835caef12b367a7"}, - {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2c14dac23c865e2d78cebd9087e148674b7154f633afd4709b4cd1520b99a61"}, - {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4527969d66a943e36ef374eda847e918077de032d58b5df84d98ffd717b6fa77"}, - {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd8256149b88e657e99f31e6d4b114c8ff2935951f1d8bb8e1fe501b224999c0"}, - {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bd1f1822d65c9054bf36d35307bf8ed4aa2d2d6827431761a813628ff671b1d"}, - {file = "pyzstd-0.16.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6733f4d373ec9ad2c1976cf06f973a3324c1f9abe236d114d6bb91165a397d"}, - {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7bec165ab6524663f00b69bfefd13a46a69fed3015754abaf81b103ec73d92c6"}, - {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4460fa6949aac6528a1ad0de8871079600b12b3ef4db49316306786a3598321"}, - {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75df79ea0315c97d88337953a17daa44023dbf6389f8151903d371513f503e3c"}, - {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:93e1d45f4a196afb6f18682c79bdd5399277ead105b67f30b35c04c207966071"}, - {file = "pyzstd-0.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:075e18b871f38a503b5d23e40a661adfc750bd4bd0bb8b208c1e290f3ceb8fa2"}, - {file = "pyzstd-0.16.2-cp313-cp313-win32.whl", hash = "sha256:9e4295eb299f8d87e3487852bca033d30332033272a801ca8130e934475e07a9"}, - {file = "pyzstd-0.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:18deedc70f858f4cf574e59f305d2a0678e54db2751a33dba9f481f91bc71c28"}, - {file = "pyzstd-0.16.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9892b707ef52f599098b1e9528df0e7849c5ec01d3e8035fb0e67de4b464839"}, - {file = "pyzstd-0.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4fbd647864341f3c174c4a6d7f20e6ea6b4be9d840fb900dc0faf0849561badc"}, - {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20ac2c15656cc6194c4fed1cb0e8159f9394d4ea1d58be755448743d2ec6c9c4"}, - {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b239fb9a20c1be3374b9a2bd183ba624fd22ad7a3f67738c0d80cda68b4ae1d3"}, - {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc52400412cdae2635e0978b8d6bcc0028cc638fdab2fd301f6d157675d26896"}, - {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b766a6aeb8dbb6c46e622e7a1aebfa9ab03838528273796941005a5ce7257b1"}, - {file = "pyzstd-0.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd4b8676052f9d59579242bf3cfe5fd02532b6a9a93ab7737c118ae3b8509dc"}, - {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1c6c0a677aac7c0e3d2d2605d4d68ffa9893fdeeb2e071040eb7c8750969d463"}, - {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:15f9c2d612e7e2023d68d321d1b479846751f792af89141931d44e82ae391394"}, - {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:11740bff847aad23beef4085a1bb767d101895881fe891f0a911aa27d43c372c"}, - {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b9067483ebe860e4130a03ee665b3d7be4ec1608b208e645d5e7eb3492379464"}, - {file = "pyzstd-0.16.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:988f0ba19b14c2fe0afefc444ac1edfb2f497b7d7c3212b2f587504cc2ec804e"}, - {file = "pyzstd-0.16.2-cp39-cp39-win32.whl", hash = "sha256:8855acb1c3e3829030b9e9e9973b19e2d70f33efb14ad5c474b4d086864c959c"}, - {file = "pyzstd-0.16.2-cp39-cp39-win_amd64.whl", hash = "sha256:018e88378df5e76f5e1d8cf4416576603b6bc4a103cbc66bb593eaac54c758de"}, - {file = "pyzstd-0.16.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b631117b97a42ff6dfd0ffc885a92fff462d7c34766b28383c57b996f863338"}, - {file = "pyzstd-0.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:56493a3fbe1b651a02102dd0902b0aa2377a732ff3544fb6fb3f114ca18db52f"}, - {file = "pyzstd-0.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1eae9bdba4a1e5d3181331f403114ff5b8ce0f4b569f48eba2b9beb2deef1e4"}, - {file = "pyzstd-0.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1be6972391c8aeecc7e61feb96ffc8e77a401bcba6ed994e7171330c45a1948"}, - {file = "pyzstd-0.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:761439d687e3a5687c2ff5c6a1190e1601362a4a3e8c6c82ff89719d51d73e19"}, - {file = "pyzstd-0.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f5fbdb8cf31b60b2dc586fecb9b73e2f172c21a0b320ed275f7b8d8a866d9003"}, - {file = "pyzstd-0.16.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:183f26e34f9becf0f2db38be9c0bfb136753d228bcb47c06c69175901bea7776"}, - {file = "pyzstd-0.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:88318b64b5205a67748148d6d244097fa6cf61fcea02ad3435511b9e7155ae16"}, - {file = "pyzstd-0.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73142aa2571b6480136a1865ebda8257e09eabbc8bcd54b222202f6fa4febe1e"}, - {file = "pyzstd-0.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d3f8877c29a97f1b1bba16f3d3ab01ad10ad3da7bad317aecf36aaf8848b37c"}, - {file = "pyzstd-0.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f25754562473ac7de856b8331ebd5964f5d85601045627a5f0bb0e4e899990"}, - {file = "pyzstd-0.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6ce17e84310080c55c02827ad9bb17893c00a845c8386a328b346f814aabd2c1"}, - {file = "pyzstd-0.16.2.tar.gz", hash = "sha256:179c1a2ea1565abf09c5f2fd72f9ce7c54b2764cf7369e05c0bfd8f1f67f63d2"}, + {file = "pyzstd-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ac857abb4c4daea71f134e74af7fe16bcfeec40911d13cf9128ddc600d46d92"}, + {file = "pyzstd-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d84e8d1cbecd3b661febf5ca8ce12c5e112cfeb8401ceedfb84ab44365298ac"}, + {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f829fa1e7daac2e45b46656bdee13923150f329e53554aeaef75cceec706dd8c"}, + {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994de7a13bb683c190a1b2a0fb99fe0c542126946f0345360582d7d5e8ce8cda"}, + {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3eb213a22823e2155aa252d9093c62ac12d7a9d698a4b37c5613f99cb9de327"}, + {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c451cfa31e70860334cc7dffe46e5178de1756642d972bc3a570fc6768673868"}, + {file = "pyzstd-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d66dc6f15249625e537ea4e5e64c195f50182556c3731f260b13c775b7888d6b"}, + {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:308d4888083913fac2b7b6f4a88f67c0773d66db37e6060971c3f173cfa92d1e"}, + {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a3b636f37af9de52efb7dd2d2f15deaeabdeeacf8e69c29bf3e7e731931e6d66"}, + {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4c07391c67b496d851b18aa29ff552a552438187900965df57f64d5cf2100c40"}, + {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e8bd12a13313ffa27347d7abe20840dcd2092852ab835a8e86008f38f11bd5ac"}, + {file = "pyzstd-0.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e27bfab45f9cdab0c336c747f493a00680a52a018a8bb7a1f787ddde4b29410"}, + {file = "pyzstd-0.17.0-cp310-cp310-win32.whl", hash = "sha256:7370c0978edfcb679419f43ec504c128463858a7ea78cf6d0538c39dfb36fce3"}, + {file = "pyzstd-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:564f7aa66cda4acd9b2a8461ff0c6a6e39a977be3e2e7317411a9f7860d7338d"}, + {file = "pyzstd-0.17.0-cp310-cp310-win_arm64.whl", hash = "sha256:fccff3a37fa4c513fe1ebf94cb9dc0369c714da22b5671f78ddcbc7ec8f581cc"}, + {file = "pyzstd-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06d1e7afafe86b90f3d763f83d2f6b6a437a8d75119fe1ff52b955eb9df04eaa"}, + {file = "pyzstd-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc827657f644e4510211b49f5dab6b04913216bc316206d98f9a75214361f16e"}, + {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecffadaa2ee516ecea3e432ebf45348fa8c360017f03b88800dd312d62ecb063"}, + {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:596de361948d3aad98a837c98fcee4598e51b608f7e0912e0e725f82e013f00f"}, + {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd3a8d0389c103e93853bf794b9a35ac5d0d11ca3e7e9f87e3305a10f6dfa6b2"}, + {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1356f72c7b8bb99b942d582b61d1a93c5065e66b6df3914dac9f2823136c3228"}, + {file = "pyzstd-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f514c339b013b0b0a2ed8ea6e44684524223bd043267d7644d7c3a70e74a0dd"}, + {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4de16306821021c2d82a45454b612e2a8683d99bfb98cff51a883af9334bea0"}, + {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aeb9759c04b6a45c1b56be21efb0a738e49b0b75c4d096a38707497a7ff2be82"}, + {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a5b31ddeada0027e67464d99f09167cf08bab5f346c3c628b2d3c84e35e239a"}, + {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8338e4e91c52af839abcf32f1f65f3b21e2597ffe411609bdbdaf10274991bd0"}, + {file = "pyzstd-0.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628e93862feb372b4700085ec4d1d389f1283ac31900af29591ae01019910ff3"}, + {file = "pyzstd-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c27773f9c95ebc891cfcf1ef282584d38cde0a96cb8d64127953ad752592d3d7"}, + {file = "pyzstd-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:c043a5766e00a2b7844705c8fa4563b7c195987120afee8f4cf594ecddf7e9ac"}, + {file = "pyzstd-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:efd371e41153ef55bf51f97e1ce4c1c0b05ceb59ed1d8972fc9aa1e9b20a790f"}, + {file = "pyzstd-0.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ac330fc4f64f97a411b6f3fc179d2fe3050b86b79140e75a9a6dd9d6d82087f"}, + {file = "pyzstd-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:725180c0c4eb2e643b7048ebfb45ddf43585b740535907f70ff6088f5eda5096"}, + {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c20fe0a60019685fa1f7137cb284f09e3f64680a503d9c0d50be4dd0a3dc5ec"}, + {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d97f7aaadc3b6e2f8e51bfa6aa203ead9c579db36d66602382534afaf296d0db"}, + {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42dcb34c5759b59721997036ff2d94210515d3ef47a9de84814f1c51a1e07e8a"}, + {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6bf05e18be6f6c003c7129e2878cffd76fcbebda4e7ebd7774e34ae140426cbf"}, + {file = "pyzstd-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f7c3a5144aa4fbccf37c30411f6b1db4c0f2cb6ad4df470b37929bffe6ca0"}, + {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9efd4007f8369fd0890701a4fc77952a0a8c4cb3bd30f362a78a1adfb3c53c12"}, + {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f8add139b5fd23b95daa844ca13118197f85bd35ce7507e92fcdce66286cc34"}, + {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:259a60e8ce9460367dcb4b34d8b66e44ca3d8c9c30d53ed59ae7037622b3bfc7"}, + {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:86011a93cc3455c5d2e35988feacffbf2fa106812a48e17eb32c2a52d25a95b3"}, + {file = "pyzstd-0.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:425c31bc3de80313054e600398e4f1bd229ee61327896d5d015e2cd0283c9012"}, + {file = "pyzstd-0.17.0-cp312-cp312-win32.whl", hash = "sha256:7c4b88183bb36eb2cebbc0352e6e9fe8e2d594f15859ae1ef13b63ebc58be158"}, + {file = "pyzstd-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c31947e0120468342d74e0fa936d43f7e1dad66a2262f939735715aa6c730e8"}, + {file = "pyzstd-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:1d0346418abcef11507356a31bef5470520f6a5a786d4e2c69109408361b1020"}, + {file = "pyzstd-0.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6cd1a1d37a7abe9c01d180dad699e3ac3889e4f48ac5dcca145cc46b04e9abd2"}, + {file = "pyzstd-0.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a44fd596eda06b6265dc0358d5b309715a93f8e96e8a4b5292c2fe0e14575b3"}, + {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a99b37453f92f0691b2454d0905bbf2f430522612f6f12bbc81133ad947eb97"}, + {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63d864e9f9e624a466070a121ace9d9cbf579eac4ed575dee3b203ab1b3cbeee"}, + {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e58bc02b055f96d1f83c791dd197d8c80253275a56cd84f917a006e9f528420d"}, + {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e62df7c0ba74618481149c849bc3ed7d551b9147e1274b4b3170bbcc0bfcc0a"}, + {file = "pyzstd-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42ecdd7136294f1becb8e57441df00eaa6dfd7444a8b0c96a1dfba5c81b066e7"}, + {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:be07a57af75f99fc39b8e2d35f8fb823ecd7ef099cd1f6203829a5094a991ae2"}, + {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0d41e6f7ec2a70dab4982157a099562de35a6735c890945b4cebb12fb7eb0be0"}, + {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f482d906426756e7cc9a43f500fee907e1b3b4e9c04d42d58fb1918c6758759b"}, + {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:827327b35605265e1d05a2f6100244415e8f2728bb75c951736c9288415908d7"}, + {file = "pyzstd-0.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a55008f80e3390e4f37bd9353830f1675f271d13d6368d2f1dc413b7c6022b3"}, + {file = "pyzstd-0.17.0-cp313-cp313-win32.whl", hash = "sha256:a4be186c0df86d4d95091c759a06582654f2b93690503b1c24d77f537d0cf5d0"}, + {file = "pyzstd-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:251a0b599bd224ec66f39165ddb2f959d0a523938e3bbfa82d8188dc03a271a2"}, + {file = "pyzstd-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce6d5fd908fd3ddec32d1c1a5a7a15b9d7737d0ef2ab20fe1e8261da61395017"}, + {file = "pyzstd-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5cb23c3c4ba4105a518cfbe8a566f9482da26f4bc8c1c865fd66e8e266be071"}, + {file = "pyzstd-0.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10b5d9215890a24f22505b68add26beeb2e3858bbe738a7ee339f0db8e29d033"}, + {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db1cff52fd24caf42a2cfb7e5d8dc822b93e9fac5dab505d0bd22e302061e2d2"}, + {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3caad3106e0e80f76acbb19c15e1e834ba6fd44dd4c82719ef8e3374f7fafd3"}, + {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7e52e1de31b935e27568742145d8b4d0f204a1605e36f4e1e2846e0d39bed98"}, + {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaa046bc9e751c4083102f3624a52bbb66e20e7aa3e28673543b22e69d9b57cd"}, + {file = "pyzstd-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cc9310bdb7cf2c70098aab40fb6bf68faaf0149110c6ef668996e7957e0147a"}, + {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3619075966456783818904f9d9e213c6fe2e583d5beb545fa1968b1848781e0f"}, + {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3844f8c7d7850580423b1b33601b016b3b913d18deb6fe14a7641b9c2714275c"}, + {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab53f91280b7b639c47bb2048e01182230e7cf3f0f0980bdb405b4241cfb705e"}, + {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:75252ee53e53a819ea7ac4271f66686018bc8b98ef12628269f099c10d881077"}, + {file = "pyzstd-0.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0795afdaa34e1ed7f3d7552100cd57a1cef9d7310b386a893e0890e9a585b427"}, + {file = "pyzstd-0.17.0-cp39-cp39-win32.whl", hash = "sha256:f7316be5a5246b6bbdd807c7a4f10382b6b02c3afc5ae6acd2e266a84f715493"}, + {file = "pyzstd-0.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:121e8fac3e24b881fed59d638100b80c34f6347c02d2f24580f633451939f2d7"}, + {file = "pyzstd-0.17.0-cp39-cp39-win_arm64.whl", hash = "sha256:fe36ccda67f73e909ac305984fe13b7b5a79296706d095a80472ada4413174c2"}, + {file = "pyzstd-0.17.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c56f99c697130f39702e07ab9fa0bb4c929c7bfe47c0a488dea732bd8a8752a"}, + {file = "pyzstd-0.17.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:152bae1b2197bcd41fc143f93acd23d474f590162547484ca04ce5874c4847de"}, + {file = "pyzstd-0.17.0-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2ddbbd7614922e52018ba3e7bb4cbe6f25b230096831d97916b8b89be8cd0cb"}, + {file = "pyzstd-0.17.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f6f3f152888825f71fd2cf2499f093fac252a5c1fa15ab8747110b3dc095f6b"}, + {file = "pyzstd-0.17.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d00a2d2bddf51c7bf32c17dc47f0f49f47ebae07c2528b9ee8abf1f318ac193"}, + {file = "pyzstd-0.17.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d79e3eff07217707a92c1a6a9841c2466bfcca4d00fea0bea968f4034c27a256"}, + {file = "pyzstd-0.17.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3ce6bac0c4c032c5200647992a8efcb9801c918633ebe11cceba946afea152d9"}, + {file = "pyzstd-0.17.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:a00998144b35be7c485a383f739fe0843a784cd96c3f1f2f53f1a249545ce49a"}, + {file = "pyzstd-0.17.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8521d7bbd00e0e1c1fd222c1369a7600fba94d24ba380618f9f75ee0c375c277"}, + {file = "pyzstd-0.17.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da65158c877eac78dcc108861d607c02fb3703195c3a177f2687e0bcdfd519d0"}, + {file = "pyzstd-0.17.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:226ca0430e2357abae1ade802585231a2959b010ec9865600e416652121ba80b"}, + {file = "pyzstd-0.17.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e3a19e8521c145a0e2cd87ca464bf83604000c5454f7e0746092834fd7de84d1"}, + {file = "pyzstd-0.17.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:56ed2de4717844ffdebb5c312ec7e7b8eb2b69eb72883bbfe472ba2c872419e6"}, + {file = "pyzstd-0.17.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc61c47ca631241081c0c99895a1feb56dab4beab37cac7d1f9f18aff06962eb"}, + {file = "pyzstd-0.17.0-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd61757a4020590dad6c20fdbf37c054ed9f349591a0d308c3c03c0303ce221"}, + {file = "pyzstd-0.17.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d6cce91a8ac8ae1aab06684a8bf0dee088405de7f451e1e89776ddc1f40074"}, + {file = "pyzstd-0.17.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc668b67a13bf6213d0a9c09edc1f4842ed680b92fc3c9361f55a904903bfd1f"}, + {file = "pyzstd-0.17.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a67d7ef18715875b31127eb90075c03ced722fd87902b34bca4b807a2ce1e4d9"}, + {file = "pyzstd-0.17.0.tar.gz", hash = "sha256:d84271f8baa66c419204c1dd115a4dec8b266f8a2921da21b81764fa208c1db6"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.13\""} + [[package]] name = "rarfile" version = "4.2" description = "RAR archive reader for Python" optional = false python-versions = ">=3.6" -groups = ["main", "test"] files = [ {file = "rarfile-4.2-py3-none-any.whl", hash = "sha256:8757e1e3757e32962e229cab2432efc1f15f210823cc96ccba0f6a39d17370c9"}, {file = "rarfile-4.2.tar.gz", hash = "sha256:8e1c8e72d0845ad2b32a47ab11a719bc2e41165ec101fd4d3fe9e92aa3f469ef"}, @@ -2618,8 +2519,6 @@ version = "0.2.2" description = "Python reflink wraps around platform specific reflink implementations" optional = true python-versions = "*" -groups = ["main"] -markers = "extra == \"reflink\"" files = [ {file = "reflink-0.2.2-cp36-cp36m-win32.whl", hash = "sha256:8435c7153af4d6e66dc8acb48a9372c8ec6f978a09cdf7b57cd6656d969e343a"}, {file = "reflink-0.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:be4787c6208faf7fc892390909cf01e34e650ea67c37bf345addefd597ed90e1"}, @@ -2635,7 +2534,6 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -2657,7 +2555,6 @@ version = "1.12.1" description = "Mock out responses from the requests package" optional = false python-versions = ">=3.5" -groups = ["test"] files = [ {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, @@ -2675,7 +2572,6 @@ version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" -groups = ["main", "test"] files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -2694,8 +2590,6 @@ version = "0.4.3" description = "Efficient signal resampling" optional = true python-versions = "*" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "resampy-0.4.3-py3-none-any.whl", hash = "sha256:ad2ed64516b140a122d96704e32bc0f92b23f45419e8b8f478e5a05f83edcebd"}, {file = "resampy-0.4.3.tar.gz", hash = "sha256:a0d1c28398f0e55994b739650afef4e3974115edbe96cd4bb81968425e916e47"}, @@ -2716,7 +2610,6 @@ version = "0.25.7" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" -groups = ["test"] files = [ {file = "responses-0.25.7-py3-none-any.whl", hash = "sha256:92ca17416c90fe6b35921f52179bff29332076bb32694c0df02dcac2c6bc043c"}, {file = "responses-0.25.7.tar.gz", hash = "sha256:8ebae11405d7a5df79ab6fd54277f6f2bc29b2d002d0dd2d5c632594d1ddcedb"}, @@ -2728,34 +2621,33 @@ requests = ">=2.30.0,<3.0" urllib3 = ">=1.25.10,<3.0" [package.extras] -tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] [[package]] name = "ruff" -version = "0.11.8" +version = "0.11.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" -groups = ["lint"] files = [ - {file = "ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3"}, - {file = "ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835"}, - {file = "ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c"}, - {file = "ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c"}, - {file = "ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219"}, - {file = "ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f"}, - {file = "ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474"}, - {file = "ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38"}, - {file = "ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458"}, - {file = "ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5"}, - {file = "ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948"}, - {file = "ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb"}, - {file = "ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c"}, - {file = "ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304"}, - {file = "ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2"}, - {file = "ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4"}, - {file = "ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2"}, - {file = "ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8"}, + {file = "ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c"}, + {file = "ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722"}, + {file = "ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1"}, + {file = "ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de"}, + {file = "ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7"}, + {file = "ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2"}, + {file = "ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271"}, + {file = "ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65"}, + {file = "ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6"}, + {file = "ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70"}, + {file = "ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381"}, + {file = "ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787"}, + {file = "ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd"}, + {file = "ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b"}, + {file = "ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a"}, + {file = "ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964"}, + {file = "ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca"}, + {file = "ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517"}, ] [[package]] @@ -2764,8 +2656,6 @@ version = "1.6.1" description = "A set of python modules for machine learning and data mining" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e"}, {file = "scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36"}, @@ -2820,8 +2710,6 @@ version = "1.13.1" description = "Fundamental algorithms for scientific computing in Python" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, @@ -2864,7 +2752,6 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "test"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -2876,7 +2763,6 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["main", "test"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2884,28 +2770,24 @@ files = [ [[package]] name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +version = "3.0.1" +description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." optional = true -python-versions = "*" -groups = ["main"] -markers = "extra == \"docs\"" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, + {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, + {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, ] [[package]] name = "soco" -version = "0.30.9" +version = "0.30.10" description = "SoCo (Sonos Controller) is a simple library to control Sonos speakers." optional = true python-versions = ">=3.6" -groups = ["main"] -markers = "extra == \"sonosupdate\"" files = [ - {file = "soco-0.30.9-py2.py3-none-any.whl", hash = "sha256:cf06a56c7431e06fe923dfd58a1217f25e7a1737b74525850859f6d30dc86a24"}, - {file = "soco-0.30.9.tar.gz", hash = "sha256:21f7a3b3f0e65aadfc90aaef69a5a428205597271b09c3d99bea8b5cb00df9da"}, + {file = "soco-0.30.10-py2.py3-none-any.whl", hash = "sha256:f62ea676e4457223a8fc5192ffe91f795f6a4a18da8aa686ef20ce6657056a0f"}, + {file = "soco-0.30.10.tar.gz", hash = "sha256:a9c8ddb53836d18a0bbb881224cc6818e1ef1b28791637378ab25ff1eb1a87c3"}, ] [package.dependencies] @@ -2917,7 +2799,7 @@ xmltodict = "*" [package.extras] events-asyncio = ["aiohttp"] -testing = ["black (>=22.12.0) ; python_version >= \"3.7\"", "coveralls", "flake8", "graphviz", "importlib-metadata (<5) ; python_version == \"3.7\"", "pylint", "pytest (>=2.5)", "pytest-cov (<2.6.0)", "requests-mock", "sphinx (==4.5.0)", "sphinx_rtd_theme", "twine", "wheel"] +testing = ["black (>=22.12.0)", "coveralls", "flake8", "graphviz", "importlib-metadata (<5)", "pylint", "pytest (>=2.5)", "pytest-cov (<2.6.0)", "requests-mock", "sphinx (==4.5.0)", "sphinx_rtd_theme", "twine", "wheel"] [[package]] name = "soundfile" @@ -2925,8 +2807,6 @@ version = "0.13.1" description = "An audio library based on libsndfile, CFFI and NumPy" optional = true python-versions = "*" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445"}, {file = "soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33"}, @@ -2948,7 +2828,6 @@ version = "2.7" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" -groups = ["main", "test"] files = [ {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, @@ -2960,8 +2839,6 @@ version = "0.5.0.post1" description = "High quality, one-dimensional sample-rate conversion library" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "soxr-0.5.0.post1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:7406d782d85f8cf64e66b65e6b7721973de8a1dc50b9e88bc2288c343a987484"}, {file = "soxr-0.5.0.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa0a382fb8d8e2afed2c1642723b2d2d1b9a6728ff89f77f3524034c8885b8c9"}, @@ -2999,8 +2876,6 @@ version = "7.4.7" description = "Python documentation generator" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, @@ -3037,8 +2912,6 @@ version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -3055,8 +2928,6 @@ version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -3073,8 +2944,6 @@ version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -3091,8 +2960,6 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = true python-versions = ">=3.5" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -3107,8 +2974,6 @@ version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -3125,8 +2990,6 @@ version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -3143,7 +3006,6 @@ version = "1.7.0" description = "module to create simple ASCII tables" optional = false python-versions = "*" -groups = ["main", "test"] files = [ {file = "texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917"}, {file = "texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638"}, @@ -3155,8 +3017,6 @@ version = "3.6.0" description = "threadpoolctl" optional = true python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"autobpm\"" files = [ {file = "threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb"}, {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, @@ -3168,7 +3028,6 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["main", "release", "test", "typing"] files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -3210,7 +3069,6 @@ version = "4.12.0.20250204" description = "Typing stubs for beautifulsoup4" optional = false python-versions = ">=3.9" -groups = ["typing"] files = [ {file = "types_beautifulsoup4-4.12.0.20250204-py3-none-any.whl", hash = "sha256:57ce9e75717b63c390fd789c787d267a67eb01fa6d800a03b9bdde2e877ed1eb"}, {file = "types_beautifulsoup4-4.12.0.20250204.tar.gz", hash = "sha256:f083d8edcbd01279f8c3995b56cfff2d01f1bb894c3b502ba118d36fbbc495bf"}, @@ -3225,7 +3083,6 @@ version = "5.0.0.20250413" description = "Typing stubs for Flask-Cors" optional = false python-versions = ">=3.9" -groups = ["typing"] files = [ {file = "types_flask_cors-5.0.0.20250413-py3-none-any.whl", hash = "sha256:8183fdba764d45a5b40214468a1d5daa0e86c4ee6042d13f38cc428308f27a64"}, {file = "types_flask_cors-5.0.0.20250413.tar.gz", hash = "sha256:b346d052f4ef3b606b73faf13e868e458f1efdbfedcbe1aba739eb2f54a6cf5f"}, @@ -3240,7 +3097,6 @@ version = "1.1.11.20241018" description = "Typing stubs for html5lib" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "types-html5lib-1.1.11.20241018.tar.gz", hash = "sha256:98042555ff78d9e3a51c77c918b1041acbb7eb6c405408d8a9e150ff5beccafa"}, {file = "types_html5lib-1.1.11.20241018-py3-none-any.whl", hash = "sha256:3f1e064d9ed2c289001ae6392c84c93833abb0816165c6ff0abfc304a779f403"}, @@ -3252,7 +3108,6 @@ version = "5.2.0.20250306" description = "Typing stubs for mock" optional = false python-versions = ">=3.9" -groups = ["typing"] files = [ {file = "types_mock-5.2.0.20250306-py3-none-any.whl", hash = "sha256:eb69fec98b8de26be1d7121623d05a2f117d1ea2e01dd30c123d07d204a15c95"}, {file = "types_mock-5.2.0.20250306.tar.gz", hash = "sha256:15882cb5cf9980587a7607e31890801223801d7997f559686805ce09b6536087"}, @@ -3264,7 +3119,6 @@ version = "10.2.0.20240822" description = "Typing stubs for Pillow" optional = false python-versions = ">=3.8" -groups = ["typing"] files = [ {file = "types-Pillow-10.2.0.20240822.tar.gz", hash = "sha256:559fb52a2ef991c326e4a0d20accb3bb63a7ba8d40eb493e0ecb0310ba52f0d3"}, {file = "types_Pillow-10.2.0.20240822-py3-none-any.whl", hash = "sha256:d9dab025aba07aeb12fd50a6799d4eac52a9603488eca09d7662543983f16c5d"}, @@ -3276,7 +3130,6 @@ version = "6.0.12.20250402" description = "Typing stubs for PyYAML" optional = false python-versions = ">=3.9" -groups = ["typing"] files = [ {file = "types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681"}, {file = "types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075"}, @@ -3288,7 +3141,6 @@ version = "2.32.0.20250328" description = "Typing stubs for requests" optional = false python-versions = ">=3.9" -groups = ["typing"] files = [ {file = "types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2"}, {file = "types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32"}, @@ -3303,7 +3155,6 @@ version = "1.26.25.14" description = "Typing stubs for urllib3" optional = false python-versions = "*" -groups = ["typing"] files = [ {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, @@ -3315,7 +3166,6 @@ version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" -groups = ["main", "test", "typing"] files = [ {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, @@ -3327,7 +3177,6 @@ version = "1.4.0" description = "ASCII transliterations of Unicode text" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021"}, {file = "Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23"}, @@ -3339,14 +3188,13 @@ version = "2.4.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["main", "test", "typing"] files = [ {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3357,7 +3205,6 @@ version = "3.1.3" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" -groups = ["main", "test", "typing"] files = [ {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, @@ -3375,8 +3222,6 @@ version = "0.14.2" description = "Makes working with XML feel like you are working with JSON" optional = true python-versions = ">=3.6" -groups = ["main"] -markers = "extra == \"sonosupdate\"" files = [ {file = "xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac"}, {file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"}, @@ -3388,19 +3233,17 @@ version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" -groups = ["main", "test", "typing"] -markers = "python_version == \"3.9\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] @@ -3431,6 +3274,6 @@ thumbnails = ["Pillow", "pyxdg"] web = ["flask", "flask-cors"] [metadata] -lock-version = "2.1" +lock-version = "2.0" python-versions = ">=3.9,<4" content-hash = "d609e83f7ffeefc12e28d627e5646aa5c1a6f5a56d7013bb649a468069550dba" diff --git a/pyproject.toml b/pyproject.toml index f83c174b4..4ce844bc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,7 +159,7 @@ build-backend = "poetry.core.masonry.api" [tool.pipx-install] poethepoet = ">=0.26" -poetry = ">=1.8" +poetry = ">=1.8,<2" [tool.poe.tasks.build] help = "Build the package" From 80cf9ea888183e061ebe37d8c07dae85b4f9e39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 14 May 2025 03:55:07 +0100 Subject: [PATCH 044/728] Update pipx-install-action action version --- .github/workflows/ci.yaml | 2 +- .github/workflows/integration_test.yaml | 2 +- .github/workflows/lint.yml | 8 ++++---- .github/workflows/make_release.yaml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a6720335f..748cf24d1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Python tools - uses: BrandonLWhite/pipx-install-action@v0.1.1 + uses: BrandonLWhite/pipx-install-action@v1.0.1 - name: Setup Python with poetry caching # poetry cache requires poetry to already be installed, weirdly uses: actions/setup-python@v5 diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 1a848bde5..eae04d1d4 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Python tools - uses: BrandonLWhite/pipx-install-action@v0.1.1 + uses: BrandonLWhite/pipx-install-action@v1.0.1 - uses: actions/setup-python@v5 with: python-version: 3.9 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9e2552ab1..16757da27 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -53,7 +53,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Python tools - uses: BrandonLWhite/pipx-install-action@v0.1.1 + uses: BrandonLWhite/pipx-install-action@v1.0.1 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} @@ -74,7 +74,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Python tools - uses: BrandonLWhite/pipx-install-action@v0.1.1 + uses: BrandonLWhite/pipx-install-action@v1.0.1 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} @@ -94,7 +94,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Python tools - uses: BrandonLWhite/pipx-install-action@v0.1.1 + uses: BrandonLWhite/pipx-install-action@v1.0.1 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} @@ -118,7 +118,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Python tools - uses: BrandonLWhite/pipx-install-action@v0.1.1 + uses: BrandonLWhite/pipx-install-action@v1.0.1 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} diff --git a/.github/workflows/make_release.yaml b/.github/workflows/make_release.yaml index e54381392..7ea2d631c 100644 --- a/.github/workflows/make_release.yaml +++ b/.github/workflows/make_release.yaml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Python tools - uses: BrandonLWhite/pipx-install-action@v0.1.1 + uses: BrandonLWhite/pipx-install-action@v1.0.1 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} @@ -50,7 +50,7 @@ jobs: ref: ${{ env.NEW_TAG }} - name: Install Python tools - uses: BrandonLWhite/pipx-install-action@v0.1.1 + uses: BrandonLWhite/pipx-install-action@v1.0.1 - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} From d487d675b9115672c484eab8a6729b1f0fd24b68 Mon Sep 17 00:00:00 2001 From: snejus <snejus@users.noreply.github.com> Date: Wed, 14 May 2025 09:53:19 +0000 Subject: [PATCH 045/728] Increment version to 2.3.1 --- beets/__init__.py | 2 +- docs/changelog.rst | 11 +++++++++++ docs/conf.py | 2 +- pyproject.toml | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 1bac81b65..8be305202 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -17,7 +17,7 @@ from sys import stderr import confuse -__version__ = "2.3.0" +__version__ = "2.3.1" __author__ = "Adrian Sampson <adrian@radbox.org>" diff --git a/docs/changelog.rst b/docs/changelog.rst index ee78db648..a18f195eb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,17 @@ Changelog goes here! Please add your entry to the bottom of one of the lists bel Unreleased ---------- +New features: + +Bug fixes: + +For packagers: + +Other changes: + +2.3.1 (May 14, 2025) +-------------------- + Bug fixes: * :doc:`/reference/pathformat`: Fixed a regression where path legalization incorrectly removed parts of user-configured path formats that followed a dot diff --git a/docs/conf.py b/docs/conf.py index fafabef70..497c5e71e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ project = "beets" copyright = "2016, Adrian Sampson" version = "2.3" -release = "2.3.0" +release = "2.3.1" pygments_style = "sphinx" diff --git a/pyproject.toml b/pyproject.toml index 4ce844bc5..8b817a078 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "beets" -version = "2.3.0" +version = "2.3.1" description = "music tagger and library organizer" authors = ["Adrian Sampson <adrian@radbox.org>"] maintainers = ["Serene-Arc"] From e6e610a3ef0f8f69d707c60d51a3d0fbcd41b850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 16 Feb 2025 10:18:17 +0000 Subject: [PATCH 046/728] Move musicbrainz to beetsplug directory --- beetsplug/albumtypes.py | 3 ++- beets/autotag/mb.py => beetsplug/musicbrainz.py | 0 test/{test_mb.py => plugins/test_musicbrainz.py} | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename beets/autotag/mb.py => beetsplug/musicbrainz.py (100%) rename test/{test_mb.py => plugins/test_musicbrainz.py} (100%) diff --git a/beetsplug/albumtypes.py b/beetsplug/albumtypes.py index b1e143a88..180773f58 100644 --- a/beetsplug/albumtypes.py +++ b/beetsplug/albumtypes.py @@ -14,10 +14,11 @@ """Adds an album template field for formatted album types.""" -from beets.autotag.mb import VARIOUS_ARTISTS_ID from beets.library import Album from beets.plugins import BeetsPlugin +from .musicbrainz import VARIOUS_ARTISTS_ID + class AlbumTypesPlugin(BeetsPlugin): """Adds an album template field for formatted album types.""" diff --git a/beets/autotag/mb.py b/beetsplug/musicbrainz.py similarity index 100% rename from beets/autotag/mb.py rename to beetsplug/musicbrainz.py diff --git a/test/test_mb.py b/test/plugins/test_musicbrainz.py similarity index 100% rename from test/test_mb.py rename to test/plugins/test_musicbrainz.py From 5dc6f45110b99f0cc8dbb94251f9b1f6d69583fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 16 Feb 2025 10:58:01 +0000 Subject: [PATCH 047/728] musicbrainz: reorder methods This will make it easier to track changes in later commits. --- beetsplug/musicbrainz.py | 290 +++++++++++++++++++-------------------- 1 file changed, 145 insertions(+), 145 deletions(-) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index 28cb66ca1..dddb1eb25 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -121,30 +121,6 @@ BROWSE_CHUNKSIZE = 100 BROWSE_MAXTRACKS = 500 -def track_url(trackid: str) -> str: - return urljoin(BASE_URL, "recording/" + trackid) - - -def album_url(albumid: str) -> str: - return urljoin(BASE_URL, "release/" + albumid) - - -def configure(): - """Set up the python-musicbrainz-ngs module according to settings - from the beets configuration. This should be called at startup. - """ - hostname = config["musicbrainz"]["host"].as_str() - https = config["musicbrainz"]["https"].get(bool) - # Only call set_hostname when a custom server is configured. Since - # musicbrainz-ngs connects to musicbrainz.org with HTTPS by default - if hostname != "musicbrainz.org": - musicbrainzngs.set_hostname(hostname, https) - musicbrainzngs.set_rate_limit( - config["musicbrainz"]["ratelimit_interval"].as_number(), - config["musicbrainz"]["ratelimit"].get(int), - ) - - def _preferred_alias(aliases: list): """Given an list of alias structures for an artist credit, select and return the user's preferred alias alias or None if no matching @@ -180,28 +156,6 @@ def _preferred_alias(aliases: list): return matches[0] -def _preferred_release_event( - release: dict[str, Any], -) -> tuple[str | None, str | None]: - """Given a release, select and return the user's preferred release - event as a tuple of (country, release_date). Fall back to the - default release event if a preferred event is not found. - """ - preferred_countries: Sequence[str] = config["match"]["preferred"][ - "countries" - ].as_str_seq() - - for country in preferred_countries: - for event in release.get("release-event-list", {}): - try: - if country in event["area"]["iso-3166-1-code-list"]: - return country, event["date"] - except KeyError: - pass - - return release.get("country"), release.get("date") - - def _multi_artist_credit( credit: list[dict], include_join_phrase: bool ) -> tuple[list[str], list[str], list[str]]: @@ -251,6 +205,10 @@ def _multi_artist_credit( ) +def track_url(trackid: str) -> str: + return urljoin(BASE_URL, "recording/" + trackid) + + def _flatten_artist_credit(credit: list[dict]) -> tuple[str, str, str]: """Given a list representing an ``artist-credit`` block, flatten the data into a triple of joined artist name strings: canonical, sort, and @@ -292,6 +250,147 @@ def _get_related_artist_names(relations, relation_type): return ", ".join(related_artists) +def album_url(albumid: str) -> str: + return urljoin(BASE_URL, "release/" + albumid) + + +def _preferred_release_event( + release: dict[str, Any], +) -> tuple[str | None, str | None]: + """Given a release, select and return the user's preferred release + event as a tuple of (country, release_date). Fall back to the + default release event if a preferred event is not found. + """ + preferred_countries: Sequence[str] = config["match"]["preferred"][ + "countries" + ].as_str_seq() + + for country in preferred_countries: + for event in release.get("release-event-list", {}): + try: + if country in event["area"]["iso-3166-1-code-list"]: + return country, event["date"] + except KeyError: + pass + + return release.get("country"), release.get("date") + + +def _set_date_str( + info: beets.autotag.hooks.AlbumInfo, + date_str: str, + original: bool = False, +): + """Given a (possibly partial) YYYY-MM-DD string and an AlbumInfo + object, set the object's release date fields appropriately. If + `original`, then set the original_year, etc., fields. + """ + if date_str: + date_parts = date_str.split("-") + for key in ("year", "month", "day"): + if date_parts: + date_part = date_parts.pop(0) + try: + date_num = int(date_part) + except ValueError: + continue + + if original: + key = "original_" + key + setattr(info, key, date_num) + + +def _parse_id(s: str) -> str | None: + """Search for a MusicBrainz ID in the given string and return it. If + no ID can be found, return None. + """ + # Find the first thing that looks like a UUID/MBID. + match = re.search("[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}", s) + if match is not None: + return match.group() if match else None + return None + + +def _is_translation(r): + _trans_key = "transl-tracklisting" + return r["type"] == _trans_key and r["direction"] == "backward" + + +def _find_actual_release_from_pseudo_release( + pseudo_rel: dict, +) -> dict | None: + try: + relations = pseudo_rel["release"]["release-relation-list"] + except KeyError: + return None + + # currently we only support trans(liter)ation's + translations = [r for r in relations if _is_translation(r)] + + if not translations: + return None + + actual_id = translations[0]["target"] + + return musicbrainzngs.get_release_by_id(actual_id, RELEASE_INCLUDES) + + +def _merge_pseudo_and_actual_album( + pseudo: beets.autotag.hooks.AlbumInfo, actual: beets.autotag.hooks.AlbumInfo +) -> beets.autotag.hooks.AlbumInfo | None: + """ + Merges a pseudo release with its actual release. + + This implementation is naive, it doesn't overwrite fields, + like status or ids. + + According to the ticket PICARD-145, the main release id should be used. + But the ticket has been in limbo since over a decade now. + It also suggests the introduction of the tag `musicbrainz_pseudoreleaseid`, + but as of this field can't be found in any official Picard docs, + hence why we did not implement that for now. + """ + merged = pseudo.copy() + from_actual = { + k: actual[k] + for k in [ + "media", + "mediums", + "country", + "catalognum", + "year", + "month", + "day", + "original_year", + "original_month", + "original_day", + "label", + "barcode", + "asin", + "style", + "genre", + ] + } + merged.update(from_actual) + return merged + + +def configure(): + """Set up the python-musicbrainz-ngs module according to settings + from the beets configuration. This should be called at startup. + """ + hostname = config["musicbrainz"]["host"].as_str() + https = config["musicbrainz"]["https"].get(bool) + # Only call set_hostname when a custom server is configured. Since + # musicbrainz-ngs connects to musicbrainz.org with HTTPS by default + if hostname != "musicbrainz.org": + musicbrainzngs.set_hostname(hostname, https) + musicbrainzngs.set_rate_limit( + config["musicbrainz"]["ratelimit_interval"].as_number(), + config["musicbrainz"]["ratelimit"].get(int), + ) + + def track_info( recording: dict, index: int | None = None, @@ -393,30 +492,6 @@ def track_info( return info -def _set_date_str( - info: beets.autotag.hooks.AlbumInfo, - date_str: str, - original: bool = False, -): - """Given a (possibly partial) YYYY-MM-DD string and an AlbumInfo - object, set the object's release date fields appropriately. If - `original`, then set the original_year, etc., fields. - """ - if date_str: - date_parts = date_str.split("-") - for key in ("year", "month", "day"): - if date_parts: - date_part = date_parts.pop(0) - try: - date_num = int(date_part) - except ValueError: - continue - - if original: - key = "original_" + key - setattr(info, key, date_num) - - def album_info(release: dict) -> beets.autotag.hooks.AlbumInfo: """Takes a MusicBrainz release result dictionary and returns a beets AlbumInfo object containing the interesting data about that release. @@ -758,81 +833,6 @@ def match_track( yield track_info(recording) -def _parse_id(s: str) -> str | None: - """Search for a MusicBrainz ID in the given string and return it. If - no ID can be found, return None. - """ - # Find the first thing that looks like a UUID/MBID. - match = re.search("[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}", s) - if match is not None: - return match.group() if match else None - return None - - -def _is_translation(r): - _trans_key = "transl-tracklisting" - return r["type"] == _trans_key and r["direction"] == "backward" - - -def _find_actual_release_from_pseudo_release( - pseudo_rel: dict, -) -> dict | None: - try: - relations = pseudo_rel["release"]["release-relation-list"] - except KeyError: - return None - - # currently we only support trans(liter)ation's - translations = [r for r in relations if _is_translation(r)] - - if not translations: - return None - - actual_id = translations[0]["target"] - - return musicbrainzngs.get_release_by_id(actual_id, RELEASE_INCLUDES) - - -def _merge_pseudo_and_actual_album( - pseudo: beets.autotag.hooks.AlbumInfo, actual: beets.autotag.hooks.AlbumInfo -) -> beets.autotag.hooks.AlbumInfo | None: - """ - Merges a pseudo release with its actual release. - - This implementation is naive, it doesn't overwrite fields, - like status or ids. - - According to the ticket PICARD-145, the main release id should be used. - But the ticket has been in limbo since over a decade now. - It also suggests the introduction of the tag `musicbrainz_pseudoreleaseid`, - but as of this field can't be found in any official Picard docs, - hence why we did not implement that for now. - """ - merged = pseudo.copy() - from_actual = { - k: actual[k] - for k in [ - "media", - "mediums", - "country", - "catalognum", - "year", - "month", - "day", - "original_year", - "original_month", - "original_day", - "label", - "barcode", - "asin", - "style", - "genre", - ] - } - merged.update(from_actual) - return merged - - def album_for_id(releaseid: str) -> beets.autotag.hooks.AlbumInfo | None: """Fetches an album by its MusicBrainz ID and returns an AlbumInfo object or None if the album is not found. May raise a From 06dde6e37e0c6e7ef6163a18f467526dc097a301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Fri, 16 May 2025 20:17:59 +0100 Subject: [PATCH 048/728] Define MusicBrainzPlugin --- beetsplug/musicbrainz.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index dddb1eb25..937f9a127 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -28,7 +28,7 @@ import musicbrainzngs import beets import beets.autotag.hooks from beets import config, logging, plugins, util -from beets.plugins import MetadataSourcePlugin +from beets.plugins import BeetsPlugin, MetadataSourcePlugin from beets.util.id_extractors import ( beatport_id_regex, deezer_id_regex, @@ -375,6 +375,10 @@ def _merge_pseudo_and_actual_album( return merged +class MusicBrainzPlugin(BeetsPlugin): + data_source = "Musicbrainz" + + def configure(): """Set up the python-musicbrainz-ngs module according to settings from the beets configuration. This should be called at startup. From 529aaac7dced71266c6d69866748a7d044ec20ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Fri, 16 May 2025 23:08:38 +0100 Subject: [PATCH 049/728] Move functionality under MusicBrainzPlugin --- beetsplug/musicbrainz.py | 916 +++++++++++++++++++-------------------- 1 file changed, 458 insertions(+), 458 deletions(-) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index 937f9a127..008c23e5a 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -379,517 +379,517 @@ class MusicBrainzPlugin(BeetsPlugin): data_source = "Musicbrainz" -def configure(): - """Set up the python-musicbrainz-ngs module according to settings - from the beets configuration. This should be called at startup. - """ - hostname = config["musicbrainz"]["host"].as_str() - https = config["musicbrainz"]["https"].get(bool) - # Only call set_hostname when a custom server is configured. Since - # musicbrainz-ngs connects to musicbrainz.org with HTTPS by default - if hostname != "musicbrainz.org": - musicbrainzngs.set_hostname(hostname, https) - musicbrainzngs.set_rate_limit( - config["musicbrainz"]["ratelimit_interval"].as_number(), - config["musicbrainz"]["ratelimit"].get(int), - ) - - -def track_info( - recording: dict, - index: int | None = None, - medium: int | None = None, - medium_index: int | None = None, - medium_total: int | None = None, -) -> beets.autotag.hooks.TrackInfo: - """Translates a MusicBrainz recording result dictionary into a beets - ``TrackInfo`` object. Three parameters are optional and are used - only for tracks that appear on releases (non-singletons): ``index``, - the overall track number; ``medium``, the disc number; - ``medium_index``, the track's index on its medium; ``medium_total``, - the number of tracks on the medium. Each number is a 1-based index. - """ - info = beets.autotag.hooks.TrackInfo( - title=recording["title"], - track_id=recording["id"], - index=index, - medium=medium, - medium_index=medium_index, - medium_total=medium_total, - data_source="MusicBrainz", - data_url=track_url(recording["id"]), - ) - - if recording.get("artist-credit"): - # Get the artist names. - ( - info.artist, - info.artist_sort, - info.artist_credit, - ) = _flatten_artist_credit(recording["artist-credit"]) - - ( - info.artists, - info.artists_sort, - info.artists_credit, - ) = _multi_artist_credit( - recording["artist-credit"], include_join_phrase=False + def configure(): + """Set up the python-musicbrainz-ngs module according to settings + from the beets configuration. This should be called at startup. + """ + hostname = config["musicbrainz"]["host"].as_str() + https = config["musicbrainz"]["https"].get(bool) + # Only call set_hostname when a custom server is configured. Since + # musicbrainz-ngs connects to musicbrainz.org with HTTPS by default + if hostname != "musicbrainz.org": + musicbrainzngs.set_hostname(hostname, https) + musicbrainzngs.set_rate_limit( + config["musicbrainz"]["ratelimit_interval"].as_number(), + config["musicbrainz"]["ratelimit"].get(int), ) - info.artists_ids = _artist_ids(recording["artist-credit"]) - info.artist_id = info.artists_ids[0] - if recording.get("artist-relation-list"): - info.remixer = _get_related_artist_names( - recording["artist-relation-list"], relation_type="remixer" + def track_info( + recording: dict, + index: int | None = None, + medium: int | None = None, + medium_index: int | None = None, + medium_total: int | None = None, + ) -> beets.autotag.hooks.TrackInfo: + """Translates a MusicBrainz recording result dictionary into a beets + ``TrackInfo`` object. Three parameters are optional and are used + only for tracks that appear on releases (non-singletons): ``index``, + the overall track number; ``medium``, the disc number; + ``medium_index``, the track's index on its medium; ``medium_total``, + the number of tracks on the medium. Each number is a 1-based index. + """ + info = beets.autotag.hooks.TrackInfo( + title=recording["title"], + track_id=recording["id"], + index=index, + medium=medium, + medium_index=medium_index, + medium_total=medium_total, + data_source="MusicBrainz", + data_url=track_url(recording["id"]), ) - if recording.get("length"): - info.length = int(recording["length"]) / 1000.0 + if recording.get("artist-credit"): + # Get the artist names. + ( + info.artist, + info.artist_sort, + info.artist_credit, + ) = _flatten_artist_credit(recording["artist-credit"]) - info.trackdisambig = recording.get("disambiguation") + ( + info.artists, + info.artists_sort, + info.artists_credit, + ) = _multi_artist_credit( + recording["artist-credit"], include_join_phrase=False + ) - if recording.get("isrc-list"): - info.isrc = ";".join(recording["isrc-list"]) + info.artists_ids = _artist_ids(recording["artist-credit"]) + info.artist_id = info.artists_ids[0] - lyricist = [] - composer = [] - composer_sort = [] - for work_relation in recording.get("work-relation-list", ()): - if work_relation["type"] != "performance": - continue - info.work = work_relation["work"]["title"] - info.mb_workid = work_relation["work"]["id"] - if "disambiguation" in work_relation["work"]: - info.work_disambig = work_relation["work"]["disambiguation"] + if recording.get("artist-relation-list"): + info.remixer = _get_related_artist_names( + recording["artist-relation-list"], relation_type="remixer" + ) - for artist_relation in work_relation["work"].get( - "artist-relation-list", () - ): + if recording.get("length"): + info.length = int(recording["length"]) / 1000.0 + + info.trackdisambig = recording.get("disambiguation") + + if recording.get("isrc-list"): + info.isrc = ";".join(recording["isrc-list"]) + + lyricist = [] + composer = [] + composer_sort = [] + for work_relation in recording.get("work-relation-list", ()): + if work_relation["type"] != "performance": + continue + info.work = work_relation["work"]["title"] + info.mb_workid = work_relation["work"]["id"] + if "disambiguation" in work_relation["work"]: + info.work_disambig = work_relation["work"]["disambiguation"] + + for artist_relation in work_relation["work"].get( + "artist-relation-list", () + ): + if "type" in artist_relation: + type = artist_relation["type"] + if type == "lyricist": + lyricist.append(artist_relation["artist"]["name"]) + elif type == "composer": + composer.append(artist_relation["artist"]["name"]) + composer_sort.append(artist_relation["artist"]["sort-name"]) + if lyricist: + info.lyricist = ", ".join(lyricist) + if composer: + info.composer = ", ".join(composer) + info.composer_sort = ", ".join(composer_sort) + + arranger = [] + for artist_relation in recording.get("artist-relation-list", ()): if "type" in artist_relation: type = artist_relation["type"] - if type == "lyricist": - lyricist.append(artist_relation["artist"]["name"]) - elif type == "composer": - composer.append(artist_relation["artist"]["name"]) - composer_sort.append(artist_relation["artist"]["sort-name"]) - if lyricist: - info.lyricist = ", ".join(lyricist) - if composer: - info.composer = ", ".join(composer) - info.composer_sort = ", ".join(composer_sort) + if type == "arranger": + arranger.append(artist_relation["artist"]["name"]) + if arranger: + info.arranger = ", ".join(arranger) - arranger = [] - for artist_relation in recording.get("artist-relation-list", ()): - if "type" in artist_relation: - type = artist_relation["type"] - if type == "arranger": - arranger.append(artist_relation["artist"]["name"]) - if arranger: - info.arranger = ", ".join(arranger) + # Supplementary fields provided by plugins + extra_trackdatas = plugins.send("mb_track_extract", data=recording) + for extra_trackdata in extra_trackdatas: + info.update(extra_trackdata) - # Supplementary fields provided by plugins - extra_trackdatas = plugins.send("mb_track_extract", data=recording) - for extra_trackdata in extra_trackdatas: - info.update(extra_trackdata) - - return info + return info -def album_info(release: dict) -> beets.autotag.hooks.AlbumInfo: - """Takes a MusicBrainz release result dictionary and returns a beets - AlbumInfo object containing the interesting data about that release. - """ - # Get artist name using join phrases. - artist_name, artist_sort_name, artist_credit_name = _flatten_artist_credit( - release["artist-credit"] - ) + def album_info(release: dict) -> beets.autotag.hooks.AlbumInfo: + """Takes a MusicBrainz release result dictionary and returns a beets + AlbumInfo object containing the interesting data about that release. + """ + # Get artist name using join phrases. + artist_name, artist_sort_name, artist_credit_name = _flatten_artist_credit( + release["artist-credit"] + ) - ( - artists_names, - artists_sort_names, - artists_credit_names, - ) = _multi_artist_credit( - release["artist-credit"], include_join_phrase=False - ) + ( + artists_names, + artists_sort_names, + artists_credit_names, + ) = _multi_artist_credit( + release["artist-credit"], include_join_phrase=False + ) - ntracks = sum(len(m["track-list"]) for m in release["medium-list"]) + ntracks = sum(len(m["track-list"]) for m in release["medium-list"]) - # The MusicBrainz API omits 'artist-relation-list' and 'work-relation-list' - # when the release has more than 500 tracks. So we use browse_recordings - # on chunks of tracks to recover the same information in this case. - if ntracks > BROWSE_MAXTRACKS: - log.debug("Album {} has too many tracks", release["id"]) - recording_list = [] - for i in range(0, ntracks, BROWSE_CHUNKSIZE): - log.debug("Retrieving tracks starting at {}", i) - recording_list.extend( - musicbrainzngs.browse_recordings( - release=release["id"], - limit=BROWSE_CHUNKSIZE, - includes=BROWSE_INCLUDES, - offset=i, - )["recording-list"] - ) - track_map = {r["id"]: r for r in recording_list} + # The MusicBrainz API omits 'artist-relation-list' and 'work-relation-list' + # when the release has more than 500 tracks. So we use browse_recordings + # on chunks of tracks to recover the same information in this case. + if ntracks > BROWSE_MAXTRACKS: + log.debug("Album {} has too many tracks", release["id"]) + recording_list = [] + for i in range(0, ntracks, BROWSE_CHUNKSIZE): + log.debug("Retrieving tracks starting at {}", i) + recording_list.extend( + musicbrainzngs.browse_recordings( + release=release["id"], + limit=BROWSE_CHUNKSIZE, + includes=BROWSE_INCLUDES, + offset=i, + )["recording-list"] + ) + track_map = {r["id"]: r for r in recording_list} + for medium in release["medium-list"]: + for recording in medium["track-list"]: + recording_info = track_map[recording["recording"]["id"]] + recording["recording"] = recording_info + + # Basic info. + track_infos = [] + index = 0 for medium in release["medium-list"]: - for recording in medium["track-list"]: - recording_info = track_map[recording["recording"]["id"]] - recording["recording"] = recording_info + disctitle = medium.get("title") + format = medium.get("format") - # Basic info. - track_infos = [] - index = 0 - for medium in release["medium-list"]: - disctitle = medium.get("title") - format = medium.get("format") - - if format in config["match"]["ignored_media"].as_str_seq(): - continue - - all_tracks = medium["track-list"] - if ( - "data-track-list" in medium - and not config["match"]["ignore_data_tracks"] - ): - all_tracks += medium["data-track-list"] - track_count = len(all_tracks) - - if "pregap" in medium: - all_tracks.insert(0, medium["pregap"]) - - for track in all_tracks: - if ( - "title" in track["recording"] - and track["recording"]["title"] in SKIPPED_TRACKS - ): + if format in config["match"]["ignored_media"].as_str_seq(): continue + all_tracks = medium["track-list"] if ( - "video" in track["recording"] - and track["recording"]["video"] == "true" - and config["match"]["ignore_video_tracks"] + "data-track-list" in medium + and not config["match"]["ignore_data_tracks"] ): - continue + all_tracks += medium["data-track-list"] + track_count = len(all_tracks) - # Basic information from the recording. - index += 1 - ti = track_info( - track["recording"], - index, - int(medium["position"]), - int(track["position"]), - track_count, - ) - ti.release_track_id = track["id"] - ti.disctitle = disctitle - ti.media = format - ti.track_alt = track["number"] + if "pregap" in medium: + all_tracks.insert(0, medium["pregap"]) - # Prefer track data, where present, over recording data. - if track.get("title"): - ti.title = track["title"] - if track.get("artist-credit"): - # Get the artist names. - ( - ti.artist, - ti.artist_sort, - ti.artist_credit, - ) = _flatten_artist_credit(track["artist-credit"]) + for track in all_tracks: + if ( + "title" in track["recording"] + and track["recording"]["title"] in SKIPPED_TRACKS + ): + continue - ( - ti.artists, - ti.artists_sort, - ti.artists_credit, - ) = _multi_artist_credit( - track["artist-credit"], include_join_phrase=False + if ( + "video" in track["recording"] + and track["recording"]["video"] == "true" + and config["match"]["ignore_video_tracks"] + ): + continue + + # Basic information from the recording. + index += 1 + ti = track_info( + track["recording"], + index, + int(medium["position"]), + int(track["position"]), + track_count, ) + ti.release_track_id = track["id"] + ti.disctitle = disctitle + ti.media = format + ti.track_alt = track["number"] - ti.artists_ids = _artist_ids(track["artist-credit"]) - ti.artist_id = ti.artists_ids[0] - if track.get("length"): - ti.length = int(track["length"]) / (1000.0) + # Prefer track data, where present, over recording data. + if track.get("title"): + ti.title = track["title"] + if track.get("artist-credit"): + # Get the artist names. + ( + ti.artist, + ti.artist_sort, + ti.artist_credit, + ) = _flatten_artist_credit(track["artist-credit"]) - track_infos.append(ti) + ( + ti.artists, + ti.artists_sort, + ti.artists_credit, + ) = _multi_artist_credit( + track["artist-credit"], include_join_phrase=False + ) - album_artist_ids = _artist_ids(release["artist-credit"]) - info = beets.autotag.hooks.AlbumInfo( - album=release["title"], - album_id=release["id"], - artist=artist_name, - artist_id=album_artist_ids[0], - artists=artists_names, - artists_ids=album_artist_ids, - tracks=track_infos, - mediums=len(release["medium-list"]), - artist_sort=artist_sort_name, - artists_sort=artists_sort_names, - artist_credit=artist_credit_name, - artists_credit=artists_credit_names, - data_source="MusicBrainz", - data_url=album_url(release["id"]), - barcode=release.get("barcode"), - ) - info.va = info.artist_id == VARIOUS_ARTISTS_ID - if info.va: - info.artist = config["va_name"].as_str() - info.asin = release.get("asin") - info.releasegroup_id = release["release-group"]["id"] - info.albumstatus = release.get("status") + ti.artists_ids = _artist_ids(track["artist-credit"]) + ti.artist_id = ti.artists_ids[0] + if track.get("length"): + ti.length = int(track["length"]) / (1000.0) - if release["release-group"].get("title"): - info.release_group_title = release["release-group"].get("title") + track_infos.append(ti) - # Get the disambiguation strings at the release and release group level. - if release["release-group"].get("disambiguation"): - info.releasegroupdisambig = release["release-group"].get( - "disambiguation" + album_artist_ids = _artist_ids(release["artist-credit"]) + info = beets.autotag.hooks.AlbumInfo( + album=release["title"], + album_id=release["id"], + artist=artist_name, + artist_id=album_artist_ids[0], + artists=artists_names, + artists_ids=album_artist_ids, + tracks=track_infos, + mediums=len(release["medium-list"]), + artist_sort=artist_sort_name, + artists_sort=artists_sort_names, + artist_credit=artist_credit_name, + artists_credit=artists_credit_names, + data_source="MusicBrainz", + data_url=album_url(release["id"]), + barcode=release.get("barcode"), ) - if release.get("disambiguation"): - info.albumdisambig = release.get("disambiguation") + info.va = info.artist_id == VARIOUS_ARTISTS_ID + if info.va: + info.artist = config["va_name"].as_str() + info.asin = release.get("asin") + info.releasegroup_id = release["release-group"]["id"] + info.albumstatus = release.get("status") - # Get the "classic" Release type. This data comes from a legacy API - # feature before MusicBrainz supported multiple release types. - if "type" in release["release-group"]: - reltype = release["release-group"]["type"] - if reltype: - info.albumtype = reltype.lower() + if release["release-group"].get("title"): + info.release_group_title = release["release-group"].get("title") - # Set the new-style "primary" and "secondary" release types. - albumtypes = [] - if "primary-type" in release["release-group"]: - rel_primarytype = release["release-group"]["primary-type"] - if rel_primarytype: - albumtypes.append(rel_primarytype.lower()) - if "secondary-type-list" in release["release-group"]: - if release["release-group"]["secondary-type-list"]: - for sec_type in release["release-group"]["secondary-type-list"]: - albumtypes.append(sec_type.lower()) - info.albumtypes = albumtypes + # Get the disambiguation strings at the release and release group level. + if release["release-group"].get("disambiguation"): + info.releasegroupdisambig = release["release-group"].get( + "disambiguation" + ) + if release.get("disambiguation"): + info.albumdisambig = release.get("disambiguation") - # Release events. - info.country, release_date = _preferred_release_event(release) - release_group_date = release["release-group"].get("first-release-date") - if not release_date: - # Fall back if release-specific date is not available. - release_date = release_group_date + # Get the "classic" Release type. This data comes from a legacy API + # feature before MusicBrainz supported multiple release types. + if "type" in release["release-group"]: + reltype = release["release-group"]["type"] + if reltype: + info.albumtype = reltype.lower() - if release_date: - _set_date_str(info, release_date, False) - _set_date_str(info, release_group_date, True) + # Set the new-style "primary" and "secondary" release types. + albumtypes = [] + if "primary-type" in release["release-group"]: + rel_primarytype = release["release-group"]["primary-type"] + if rel_primarytype: + albumtypes.append(rel_primarytype.lower()) + if "secondary-type-list" in release["release-group"]: + if release["release-group"]["secondary-type-list"]: + for sec_type in release["release-group"]["secondary-type-list"]: + albumtypes.append(sec_type.lower()) + info.albumtypes = albumtypes - # Label name. - if release.get("label-info-list"): - label_info = release["label-info-list"][0] - if label_info.get("label"): - label = label_info["label"]["name"] - if label != "[no label]": - info.label = label - info.catalognum = label_info.get("catalog-number") + # Release events. + info.country, release_date = _preferred_release_event(release) + release_group_date = release["release-group"].get("first-release-date") + if not release_date: + # Fall back if release-specific date is not available. + release_date = release_group_date - # Text representation data. - if release.get("text-representation"): - rep = release["text-representation"] - info.script = rep.get("script") - info.language = rep.get("language") + if release_date: + _set_date_str(info, release_date, False) + _set_date_str(info, release_group_date, True) - # Media (format). - if release["medium-list"]: - # If all media are the same, use that medium name - if len({m.get("format") for m in release["medium-list"]}) == 1: - info.media = release["medium-list"][0].get("format") - # Otherwise, let's just call it "Media" + # Label name. + if release.get("label-info-list"): + label_info = release["label-info-list"][0] + if label_info.get("label"): + label = label_info["label"]["name"] + if label != "[no label]": + info.label = label + info.catalognum = label_info.get("catalog-number") + + # Text representation data. + if release.get("text-representation"): + rep = release["text-representation"] + info.script = rep.get("script") + info.language = rep.get("language") + + # Media (format). + if release["medium-list"]: + # If all media are the same, use that medium name + if len({m.get("format") for m in release["medium-list"]}) == 1: + info.media = release["medium-list"][0].get("format") + # Otherwise, let's just call it "Media" + else: + info.media = "Media" + + if config["musicbrainz"]["genres"]: + sources = [ + release["release-group"].get("tag-list", []), + release.get("tag-list", []), + ] + genres: Counter[str] = Counter() + for source in sources: + for genreitem in source: + genres[genreitem["name"]] += int(genreitem["count"]) + info.genre = "; ".join( + genre + for genre, _count in sorted(genres.items(), key=lambda g: -g[1]) + ) + + # We might find links to external sources (Discogs, Bandcamp, ...) + external_ids = config["musicbrainz"]["external_ids"].get() + wanted_sources = {site for site, wanted in external_ids.items() if wanted} + if wanted_sources and (url_rels := release.get("url-relation-list")): + urls = {} + + for source, url in product(wanted_sources, url_rels): + if f"{source}.com" in (target := url["target"]): + urls[source] = target + log.debug( + "Found link to {} release via MusicBrainz", + source.capitalize(), + ) + + if "discogs" in urls: + info.discogs_albumid = extract_discogs_id_regex(urls["discogs"]) + if "bandcamp" in urls: + info.bandcamp_album_id = urls["bandcamp"] + if "spotify" in urls: + info.spotify_album_id = MetadataSourcePlugin._get_id( + "album", urls["spotify"], spotify_id_regex + ) + if "deezer" in urls: + info.deezer_album_id = MetadataSourcePlugin._get_id( + "album", urls["deezer"], deezer_id_regex + ) + if "beatport" in urls: + info.beatport_album_id = MetadataSourcePlugin._get_id( + "album", urls["beatport"], beatport_id_regex + ) + if "tidal" in urls: + info.tidal_album_id = urls["tidal"].split("/")[-1] + + extra_albumdatas = plugins.send("mb_album_extract", data=release) + for extra_albumdata in extra_albumdatas: + info.update(extra_albumdata) + + return info + + + def match_album( + artist: str, + album: str, + tracks: int | None = None, + extra_tags: dict[str, Any] | None = None, + ) -> Iterator[beets.autotag.hooks.AlbumInfo]: + """Searches for a single album ("release" in MusicBrainz parlance) + and returns an iterator over AlbumInfo objects. May raise a + MusicBrainzAPIError. + + The query consists of an artist name, an album name, and, + optionally, a number of tracks on the album and any other extra tags. + """ + # Build search criteria. + criteria = {"release": album.lower().strip()} + if artist is not None: + criteria["artist"] = artist.lower().strip() else: - info.media = "Media" + # Various Artists search. + criteria["arid"] = VARIOUS_ARTISTS_ID + if tracks is not None: + criteria["tracks"] = str(tracks) - if config["musicbrainz"]["genres"]: - sources = [ - release["release-group"].get("tag-list", []), - release.get("tag-list", []), - ] - genres: Counter[str] = Counter() - for source in sources: - for genreitem in source: - genres[genreitem["name"]] += int(genreitem["count"]) - info.genre = "; ".join( - genre - for genre, _count in sorted(genres.items(), key=lambda g: -g[1]) - ) + # Additional search cues from existing metadata. + if extra_tags: + for tag, value in extra_tags.items(): + key = FIELDS_TO_MB_KEYS[tag] + value = str(value).lower().strip() + if key == "catno": + value = value.replace(" ", "") + if value: + criteria[key] = value - # We might find links to external sources (Discogs, Bandcamp, ...) - external_ids = config["musicbrainz"]["external_ids"].get() - wanted_sources = {site for site, wanted in external_ids.items() if wanted} - if wanted_sources and (url_rels := release.get("url-relation-list")): - urls = {} + # Abort if we have no search terms. + if not any(criteria.values()): + return - for source, url in product(wanted_sources, url_rels): - if f"{source}.com" in (target := url["target"]): - urls[source] = target - log.debug( - "Found link to {} release via MusicBrainz", - source.capitalize(), - ) - - if "discogs" in urls: - info.discogs_albumid = extract_discogs_id_regex(urls["discogs"]) - if "bandcamp" in urls: - info.bandcamp_album_id = urls["bandcamp"] - if "spotify" in urls: - info.spotify_album_id = MetadataSourcePlugin._get_id( - "album", urls["spotify"], spotify_id_regex + try: + log.debug("Searching for MusicBrainz releases with: {!r}", criteria) + res = musicbrainzngs.search_releases( + limit=config["musicbrainz"]["searchlimit"].get(int), **criteria ) - if "deezer" in urls: - info.deezer_album_id = MetadataSourcePlugin._get_id( - "album", urls["deezer"], deezer_id_regex + except musicbrainzngs.MusicBrainzError as exc: + raise MusicBrainzAPIError( + exc, "release search", criteria, traceback.format_exc() ) - if "beatport" in urls: - info.beatport_album_id = MetadataSourcePlugin._get_id( - "album", urls["beatport"], beatport_id_regex + for release in res["release-list"]: + # The search result is missing some data (namely, the tracks), + # so we just use the ID and fetch the rest of the information. + albuminfo = album_for_id(release["id"]) + if albuminfo is not None: + yield albuminfo + + + def match_track( + artist: str, + title: str, + ) -> Iterator[beets.autotag.hooks.TrackInfo]: + """Searches for a single track and returns an iterable of TrackInfo + objects. May raise a MusicBrainzAPIError. + """ + criteria = { + "artist": artist.lower().strip(), + "recording": title.lower().strip(), + } + + if not any(criteria.values()): + return + + try: + res = musicbrainzngs.search_recordings( + limit=config["musicbrainz"]["searchlimit"].get(int), **criteria ) - if "tidal" in urls: - info.tidal_album_id = urls["tidal"].split("/")[-1] - - extra_albumdatas = plugins.send("mb_album_extract", data=release) - for extra_albumdata in extra_albumdatas: - info.update(extra_albumdata) - - return info + except musicbrainzngs.MusicBrainzError as exc: + raise MusicBrainzAPIError( + exc, "recording search", criteria, traceback.format_exc() + ) + for recording in res["recording-list"]: + yield track_info(recording) -def match_album( - artist: str, - album: str, - tracks: int | None = None, - extra_tags: dict[str, Any] | None = None, -) -> Iterator[beets.autotag.hooks.AlbumInfo]: - """Searches for a single album ("release" in MusicBrainz parlance) - and returns an iterator over AlbumInfo objects. May raise a - MusicBrainzAPIError. + def album_for_id(releaseid: str) -> beets.autotag.hooks.AlbumInfo | None: + """Fetches an album by its MusicBrainz ID and returns an AlbumInfo + object or None if the album is not found. May raise a + MusicBrainzAPIError. + """ + log.debug("Requesting MusicBrainz release {}", releaseid) + albumid = _parse_id(releaseid) + if not albumid: + log.debug("Invalid MBID ({0}).", releaseid) + return None + try: + res = musicbrainzngs.get_release_by_id(albumid, RELEASE_INCLUDES) - The query consists of an artist name, an album name, and, - optionally, a number of tracks on the album and any other extra tags. - """ - # Build search criteria. - criteria = {"release": album.lower().strip()} - if artist is not None: - criteria["artist"] = artist.lower().strip() - else: - # Various Artists search. - criteria["arid"] = VARIOUS_ARTISTS_ID - if tracks is not None: - criteria["tracks"] = str(tracks) + # resolve linked release relations + actual_res = None - # Additional search cues from existing metadata. - if extra_tags: - for tag, value in extra_tags.items(): - key = FIELDS_TO_MB_KEYS[tag] - value = str(value).lower().strip() - if key == "catno": - value = value.replace(" ", "") - if value: - criteria[key] = value + if res["release"].get("status") == "Pseudo-Release": + actual_res = _find_actual_release_from_pseudo_release(res) - # Abort if we have no search terms. - if not any(criteria.values()): - return + except musicbrainzngs.ResponseError: + log.debug("Album ID match failed.") + return None + except musicbrainzngs.MusicBrainzError as exc: + raise MusicBrainzAPIError( + exc, "get release by ID", albumid, traceback.format_exc() + ) - try: - log.debug("Searching for MusicBrainz releases with: {!r}", criteria) - res = musicbrainzngs.search_releases( - limit=config["musicbrainz"]["searchlimit"].get(int), **criteria - ) - except musicbrainzngs.MusicBrainzError as exc: - raise MusicBrainzAPIError( - exc, "release search", criteria, traceback.format_exc() - ) - for release in res["release-list"]: - # The search result is missing some data (namely, the tracks), - # so we just use the ID and fetch the rest of the information. - albuminfo = album_for_id(release["id"]) - if albuminfo is not None: - yield albuminfo + # release is potentially a pseudo release + release = album_info(res["release"]) + + # should be None unless we're dealing with a pseudo release + if actual_res is not None: + actual_release = album_info(actual_res["release"]) + return _merge_pseudo_and_actual_album(release, actual_release) + else: + return release -def match_track( - artist: str, - title: str, -) -> Iterator[beets.autotag.hooks.TrackInfo]: - """Searches for a single track and returns an iterable of TrackInfo - objects. May raise a MusicBrainzAPIError. - """ - criteria = { - "artist": artist.lower().strip(), - "recording": title.lower().strip(), - } - - if not any(criteria.values()): - return - - try: - res = musicbrainzngs.search_recordings( - limit=config["musicbrainz"]["searchlimit"].get(int), **criteria - ) - except musicbrainzngs.MusicBrainzError as exc: - raise MusicBrainzAPIError( - exc, "recording search", criteria, traceback.format_exc() - ) - for recording in res["recording-list"]: - yield track_info(recording) - - -def album_for_id(releaseid: str) -> beets.autotag.hooks.AlbumInfo | None: - """Fetches an album by its MusicBrainz ID and returns an AlbumInfo - object or None if the album is not found. May raise a - MusicBrainzAPIError. - """ - log.debug("Requesting MusicBrainz release {}", releaseid) - albumid = _parse_id(releaseid) - if not albumid: - log.debug("Invalid MBID ({0}).", releaseid) - return None - try: - res = musicbrainzngs.get_release_by_id(albumid, RELEASE_INCLUDES) - - # resolve linked release relations - actual_res = None - - if res["release"].get("status") == "Pseudo-Release": - actual_res = _find_actual_release_from_pseudo_release(res) - - except musicbrainzngs.ResponseError: - log.debug("Album ID match failed.") - return None - except musicbrainzngs.MusicBrainzError as exc: - raise MusicBrainzAPIError( - exc, "get release by ID", albumid, traceback.format_exc() - ) - - # release is potentially a pseudo release - release = album_info(res["release"]) - - # should be None unless we're dealing with a pseudo release - if actual_res is not None: - actual_release = album_info(actual_res["release"]) - return _merge_pseudo_and_actual_album(release, actual_release) - else: - return release - - -def track_for_id(releaseid: str) -> beets.autotag.hooks.TrackInfo | None: - """Fetches a track by its MusicBrainz ID. Returns a TrackInfo object - or None if no track is found. May raise a MusicBrainzAPIError. - """ - trackid = _parse_id(releaseid) - if not trackid: - log.debug("Invalid MBID ({0}).", releaseid) - return None - try: - res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES) - except musicbrainzngs.ResponseError: - log.debug("Track ID match failed.") - return None - except musicbrainzngs.MusicBrainzError as exc: - raise MusicBrainzAPIError( - exc, "get recording by ID", trackid, traceback.format_exc() - ) - return track_info(res["recording"]) + def track_for_id(releaseid: str) -> beets.autotag.hooks.TrackInfo | None: + """Fetches a track by its MusicBrainz ID. Returns a TrackInfo object + or None if no track is found. May raise a MusicBrainzAPIError. + """ + trackid = _parse_id(releaseid) + if not trackid: + log.debug("Invalid MBID ({0}).", releaseid) + return None + try: + res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES) + except musicbrainzngs.ResponseError: + log.debug("Track ID match failed.") + return None + except musicbrainzngs.MusicBrainzError as exc: + raise MusicBrainzAPIError( + exc, "get recording by ID", trackid, traceback.format_exc() + ) + return track_info(res["recording"]) From fd62d6a0b8fe3f08465626fc04d340d155fbcbe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Fri, 16 May 2025 23:12:58 +0100 Subject: [PATCH 050/728] Integrate functionality with BeetsPlugin shenanigans --- beetsplug/musicbrainz.py | 71 +++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index 008c23e5a..657888644 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -39,6 +39,8 @@ from beets.util.id_extractors import ( if TYPE_CHECKING: from collections.abc import Iterator, Sequence + from beets.library import Item + VARIOUS_ARTISTS_ID = "89ad4ac3-39f7-470e-963a-56509c546377" BASE_URL = "https://musicbrainz.org/" @@ -378,8 +380,7 @@ def _merge_pseudo_and_actual_album( class MusicBrainzPlugin(BeetsPlugin): data_source = "Musicbrainz" - - def configure(): + def __init__(self): """Set up the python-musicbrainz-ngs module according to settings from the beets configuration. This should be called at startup. """ @@ -394,8 +395,8 @@ class MusicBrainzPlugin(BeetsPlugin): config["musicbrainz"]["ratelimit"].get(int), ) - def track_info( + self, recording: dict, index: int | None = None, medium: int | None = None, @@ -472,7 +473,9 @@ class MusicBrainzPlugin(BeetsPlugin): lyricist.append(artist_relation["artist"]["name"]) elif type == "composer": composer.append(artist_relation["artist"]["name"]) - composer_sort.append(artist_relation["artist"]["sort-name"]) + composer_sort.append( + artist_relation["artist"]["sort-name"] + ) if lyricist: info.lyricist = ", ".join(lyricist) if composer: @@ -495,14 +498,13 @@ class MusicBrainzPlugin(BeetsPlugin): return info - - def album_info(release: dict) -> beets.autotag.hooks.AlbumInfo: + def album_info(self, release: dict) -> beets.autotag.hooks.AlbumInfo: """Takes a MusicBrainz release result dictionary and returns a beets AlbumInfo object containing the interesting data about that release. """ # Get artist name using join phrases. - artist_name, artist_sort_name, artist_credit_name = _flatten_artist_credit( - release["artist-credit"] + artist_name, artist_sort_name, artist_credit_name = ( + _flatten_artist_credit(release["artist-credit"]) ) ( @@ -574,7 +576,7 @@ class MusicBrainzPlugin(BeetsPlugin): # Basic information from the recording. index += 1 - ti = track_info( + ti = self.track_info( track["recording"], index, int(medium["position"]), @@ -718,7 +720,9 @@ class MusicBrainzPlugin(BeetsPlugin): # We might find links to external sources (Discogs, Bandcamp, ...) external_ids = config["musicbrainz"]["external_ids"].get() - wanted_sources = {site for site, wanted in external_ids.items() if wanted} + wanted_sources = { + site for site, wanted in external_ids.items() if wanted + } if wanted_sources and (url_rels := release.get("url-relation-list")): urls = {} @@ -755,11 +759,12 @@ class MusicBrainzPlugin(BeetsPlugin): return info - - def match_album( + def candidates( + self, + items: list[Item], artist: str, album: str, - tracks: int | None = None, + va_likely: bool, extra_tags: dict[str, Any] | None = None, ) -> Iterator[beets.autotag.hooks.AlbumInfo]: """Searches for a single album ("release" in MusicBrainz parlance) @@ -776,8 +781,8 @@ class MusicBrainzPlugin(BeetsPlugin): else: # Various Artists search. criteria["arid"] = VARIOUS_ARTISTS_ID - if tracks is not None: - criteria["tracks"] = str(tracks) + if track_count := len(items): + criteria["tracks"] = str(track_count) # Additional search cues from existing metadata. if extra_tags: @@ -805,14 +810,12 @@ class MusicBrainzPlugin(BeetsPlugin): for release in res["release-list"]: # The search result is missing some data (namely, the tracks), # so we just use the ID and fetch the rest of the information. - albuminfo = album_for_id(release["id"]) + albuminfo = self.album_for_id(release["id"]) if albuminfo is not None: yield albuminfo - - def match_track( - artist: str, - title: str, + def item_candidates( + self, item: Item, artist: str, title: str ) -> Iterator[beets.autotag.hooks.TrackInfo]: """Searches for a single track and returns an iterable of TrackInfo objects. May raise a MusicBrainzAPIError. @@ -834,18 +837,19 @@ class MusicBrainzPlugin(BeetsPlugin): exc, "recording search", criteria, traceback.format_exc() ) for recording in res["recording-list"]: - yield track_info(recording) + yield self.track_info(recording) - - def album_for_id(releaseid: str) -> beets.autotag.hooks.AlbumInfo | None: + def album_for_id( + self, album_id: str + ) -> beets.autotag.hooks.AlbumInfo | None: """Fetches an album by its MusicBrainz ID and returns an AlbumInfo object or None if the album is not found. May raise a MusicBrainzAPIError. """ - log.debug("Requesting MusicBrainz release {}", releaseid) - albumid = _parse_id(releaseid) + log.debug("Requesting MusicBrainz release {}", album_id) + albumid = _parse_id(album_id) if not albumid: - log.debug("Invalid MBID ({0}).", releaseid) + log.debug("Invalid MBID ({0}).", album_id) return None try: res = musicbrainzngs.get_release_by_id(albumid, RELEASE_INCLUDES) @@ -865,23 +869,24 @@ class MusicBrainzPlugin(BeetsPlugin): ) # release is potentially a pseudo release - release = album_info(res["release"]) + release = self.album_info(res["release"]) # should be None unless we're dealing with a pseudo release if actual_res is not None: - actual_release = album_info(actual_res["release"]) + actual_release = self.album_info(actual_res["release"]) return _merge_pseudo_and_actual_album(release, actual_release) else: return release - - def track_for_id(releaseid: str) -> beets.autotag.hooks.TrackInfo | None: + def track_for_id( + self, track_id: str + ) -> beets.autotag.hooks.TrackInfo | None: """Fetches a track by its MusicBrainz ID. Returns a TrackInfo object or None if no track is found. May raise a MusicBrainzAPIError. """ - trackid = _parse_id(releaseid) + trackid = _parse_id(track_id) if not trackid: - log.debug("Invalid MBID ({0}).", releaseid) + log.debug("Invalid MBID ({0}).", track_id) return None try: res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES) @@ -892,4 +897,4 @@ class MusicBrainzPlugin(BeetsPlugin): raise MusicBrainzAPIError( exc, "get recording by ID", trackid, traceback.format_exc() ) - return track_info(res["recording"]) + return self.track_info(res["recording"]) From d8067b219b6ee732f8a4e5c9b6fc690a274e2c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 17 Feb 2025 01:44:48 +0000 Subject: [PATCH 051/728] musicbrainz: use self.config and self._log --- beetsplug/musicbrainz.py | 41 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index 657888644..fdbdf1598 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -27,7 +27,7 @@ import musicbrainzngs import beets import beets.autotag.hooks -from beets import config, logging, plugins, util +from beets import config, plugins, util from beets.plugins import BeetsPlugin, MetadataSourcePlugin from beets.util.id_extractors import ( beatport_id_regex, @@ -76,8 +76,6 @@ class MusicBrainzAPIError(util.HumanReadableError): ) -log = logging.getLogger("beets") - RELEASE_INCLUDES = list( { "artists", @@ -384,15 +382,16 @@ class MusicBrainzPlugin(BeetsPlugin): """Set up the python-musicbrainz-ngs module according to settings from the beets configuration. This should be called at startup. """ - hostname = config["musicbrainz"]["host"].as_str() - https = config["musicbrainz"]["https"].get(bool) + super().__init__() + hostname = self.config["host"].as_str() + https = self.config["https"].get(bool) # Only call set_hostname when a custom server is configured. Since # musicbrainz-ngs connects to musicbrainz.org with HTTPS by default if hostname != "musicbrainz.org": musicbrainzngs.set_hostname(hostname, https) musicbrainzngs.set_rate_limit( - config["musicbrainz"]["ratelimit_interval"].as_number(), - config["musicbrainz"]["ratelimit"].get(int), + self.config["ratelimit_interval"].as_number(), + self.config["ratelimit"].get(int), ) def track_info( @@ -521,10 +520,10 @@ class MusicBrainzPlugin(BeetsPlugin): # when the release has more than 500 tracks. So we use browse_recordings # on chunks of tracks to recover the same information in this case. if ntracks > BROWSE_MAXTRACKS: - log.debug("Album {} has too many tracks", release["id"]) + self._log.debug("Album {} has too many tracks", release["id"]) recording_list = [] for i in range(0, ntracks, BROWSE_CHUNKSIZE): - log.debug("Retrieving tracks starting at {}", i) + self._log.debug("Retrieving tracks starting at {}", i) recording_list.extend( musicbrainzngs.browse_recordings( release=release["id"], @@ -704,7 +703,7 @@ class MusicBrainzPlugin(BeetsPlugin): else: info.media = "Media" - if config["musicbrainz"]["genres"]: + if self.config["genres"]: sources = [ release["release-group"].get("tag-list", []), release.get("tag-list", []), @@ -719,7 +718,7 @@ class MusicBrainzPlugin(BeetsPlugin): ) # We might find links to external sources (Discogs, Bandcamp, ...) - external_ids = config["musicbrainz"]["external_ids"].get() + external_ids = self.config["external_ids"].get() wanted_sources = { site for site, wanted in external_ids.items() if wanted } @@ -729,7 +728,7 @@ class MusicBrainzPlugin(BeetsPlugin): for source, url in product(wanted_sources, url_rels): if f"{source}.com" in (target := url["target"]): urls[source] = target - log.debug( + self._log.debug( "Found link to {} release via MusicBrainz", source.capitalize(), ) @@ -799,9 +798,11 @@ class MusicBrainzPlugin(BeetsPlugin): return try: - log.debug("Searching for MusicBrainz releases with: {!r}", criteria) + self._log.debug( + "Searching for MusicBrainz releases with: {!r}", criteria + ) res = musicbrainzngs.search_releases( - limit=config["musicbrainz"]["searchlimit"].get(int), **criteria + limit=self.config["searchlimit"].get(int), **criteria ) except musicbrainzngs.MusicBrainzError as exc: raise MusicBrainzAPIError( @@ -830,7 +831,7 @@ class MusicBrainzPlugin(BeetsPlugin): try: res = musicbrainzngs.search_recordings( - limit=config["musicbrainz"]["searchlimit"].get(int), **criteria + limit=self.config["searchlimit"].get(int), **criteria ) except musicbrainzngs.MusicBrainzError as exc: raise MusicBrainzAPIError( @@ -846,10 +847,10 @@ class MusicBrainzPlugin(BeetsPlugin): object or None if the album is not found. May raise a MusicBrainzAPIError. """ - log.debug("Requesting MusicBrainz release {}", album_id) + self._log.debug("Requesting MusicBrainz release {}", album_id) albumid = _parse_id(album_id) if not albumid: - log.debug("Invalid MBID ({0}).", album_id) + self._log.debug("Invalid MBID ({0}).", album_id) return None try: res = musicbrainzngs.get_release_by_id(albumid, RELEASE_INCLUDES) @@ -861,7 +862,7 @@ class MusicBrainzPlugin(BeetsPlugin): actual_res = _find_actual_release_from_pseudo_release(res) except musicbrainzngs.ResponseError: - log.debug("Album ID match failed.") + self._log.debug("Album ID match failed.") return None except musicbrainzngs.MusicBrainzError as exc: raise MusicBrainzAPIError( @@ -886,12 +887,12 @@ class MusicBrainzPlugin(BeetsPlugin): """ trackid = _parse_id(track_id) if not trackid: - log.debug("Invalid MBID ({0}).", track_id) + self._log.debug("Invalid MBID ({0}).", track_id) return None try: res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES) except musicbrainzngs.ResponseError: - log.debug("Track ID match failed.") + self._log.debug("Track ID match failed.") return None except musicbrainzngs.MusicBrainzError as exc: raise MusicBrainzAPIError( From 2fcb48d7a48775aad74f117bc7b4b21a6fcb8006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 17 Feb 2025 20:44:01 +0000 Subject: [PATCH 052/728] Remove ...for_mbid methods and simplify the rest --- beets/autotag/hooks.py | 94 ++++---------------------------- beets/autotag/match.py | 20 +++---- beets/ui/__init__.py | 4 -- beetsplug/chroma.py | 15 +++-- test/plugins/test_musicbrainz.py | 2 +- 5 files changed, 30 insertions(+), 105 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 33606020d..530b5d7ad 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -18,17 +18,16 @@ from __future__ import annotations import re from functools import total_ordering -from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar +from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar from jellyfish import levenshtein_distance from unidecode import unidecode from beets import config, logging, plugins -from beets.autotag import mb from beets.util import as_string, cached_classproperty if TYPE_CHECKING: - from collections.abc import Iterable, Iterator + from collections.abc import Iterator from beets.library import Item @@ -596,94 +595,21 @@ class TrackMatch(NamedTuple): # Aggregation of sources. -def album_for_mbid(release_id: str) -> AlbumInfo | None: - """Get an AlbumInfo object for a MusicBrainz release ID. Return None - if the ID is not found. - """ - try: - if album := mb.album_for_id(release_id): - plugins.send("albuminfo_received", info=album) - return album - except mb.MusicBrainzAPIError as exc: - exc.log(log) - return None - - -def track_for_mbid(recording_id: str) -> TrackInfo | None: - """Get a TrackInfo object for a MusicBrainz recording ID. Return None - if the ID is not found. - """ - try: - if track := mb.track_for_id(recording_id): - plugins.send("trackinfo_received", info=track) - return track - except mb.MusicBrainzAPIError as exc: - exc.log(log) - return None - - -def album_for_id(_id: str) -> AlbumInfo | None: +def album_for_id(*args, **kwargs) -> AlbumInfo | None: """Get AlbumInfo object for the given ID string.""" - return album_for_mbid(_id) or plugins.album_for_id(_id) + return plugins.album_for_id(*args, **kwargs) -def track_for_id(_id: str) -> TrackInfo | None: +def track_for_id(*args, **kwargs) -> TrackInfo | None: """Get TrackInfo object for the given ID string.""" - return track_for_mbid(_id) or plugins.track_for_id(_id) - - -def invoke_mb(call_func: Callable, *args): - try: - return call_func(*args) - except mb.MusicBrainzAPIError as exc: - exc.log(log) - return () + return plugins.track_for_id(*args, **kwargs) @plugins.notify_info_yielded("albuminfo_received") -def album_candidates( - items: list[Item], - artist: str, - album: str, - va_likely: bool, - extra_tags: dict, -) -> Iterable[tuple]: - """Search for album matches. ``items`` is a list of Item objects - that make up the album. ``artist`` and ``album`` are the respective - names (strings), which may be derived from the item list or may be - entered by the user. ``va_likely`` is a boolean indicating whether - the album is likely to be a "various artists" release. ``extra_tags`` - is an optional dictionary of additional tags used to further - constrain the search. - """ - - if config["musicbrainz"]["enabled"]: - # Base candidates if we have album and artist to match. - if artist and album: - yield from invoke_mb( - mb.match_album, artist, album, len(items), extra_tags - ) - - # Also add VA matches from MusicBrainz where appropriate. - if va_likely and album: - yield from invoke_mb( - mb.match_album, None, album, len(items), extra_tags - ) - - # Candidates from plugins. - yield from plugins.candidates(items, artist, album, va_likely, extra_tags) +def album_candidates(*args, **kwargs) -> Iterator[AlbumInfo]: + yield from plugins.candidates(*args, **kwargs) @plugins.notify_info_yielded("trackinfo_received") -def item_candidates(item: Item, artist: str, title: str) -> Iterable[tuple]: - """Search for item matches. ``item`` is the Item to be matched. - ``artist`` and ``title`` are strings and either reflect the item or - are specified by the user. - """ - - # MusicBrainz candidates. - if config["musicbrainz"]["enabled"] and artist and title: - yield from invoke_mb(mb.match_track, artist, title) - - # Plugin candidates. - yield from plugins.item_candidates(item, artist, title) +def item_candidates(*args, **kwargs) -> Iterator[TrackInfo]: + yield from plugins.item_candidates(*args, **kwargs) diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 433093def..5333b93fe 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -335,8 +335,8 @@ def distance( return dist -def match_by_id(items: Iterable[Item]): - """If the items are tagged with a MusicBrainz album ID, returns an +def match_by_id(items: Iterable[Item]) -> AlbumInfo | None: + """If the items are tagged with an external source ID, return an AlbumInfo object for the corresponding album. Otherwise, returns None. """ @@ -356,7 +356,7 @@ def match_by_id(items: Iterable[Item]): return None # If all album IDs are equal, look up the album. log.debug("Searching for discovered album ID: {0}", first) - return hooks.album_for_mbid(first) + return hooks.album_for_id(first) def _recommendation( @@ -517,9 +517,8 @@ def tag_album( # Use existing metadata or text search. else: # Try search based on current ID. - id_info = match_by_id(items) - if id_info: - _add_candidate(items, candidates, id_info) + if info := match_by_id(items): + _add_candidate(items, candidates, info) rec = _recommendation(list(candidates.values())) log.debug("Album ID match recommendation is {0}", rec) if candidates and not config["import"]["timid"]: @@ -576,17 +575,16 @@ def tag_item( """Find metadata for a single track. Return a `Proposal` consisting of `TrackMatch` objects. - `search_artist` and `search_title` may be used - to override the current metadata for the purposes of the MusicBrainz - title. `search_ids` may be used for restricting the search to a list - of metadata backend IDs. + `search_artist` and `search_title` may be used to override the item + metadata in the search query. `search_ids` may be used for restricting the + search to a list of metadata backend IDs. """ # Holds candidates found so far: keys are MBIDs; values are # (distance, TrackInfo) pairs. candidates = {} rec: Recommendation | None = None - # First, try matching by MusicBrainz ID. + # First, try matching by the external source ID. trackids = search_ids or [t for t in [item.mb_trackid] if t] if trackids: for trackid in trackids: diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 8cc5de309..fef2ae0c5 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -32,7 +32,6 @@ from typing import Any, Callable import confuse from beets import config, library, logging, plugins, util -from beets.autotag import mb from beets.dbcore import db from beets.dbcore import query as db_query from beets.util import as_string @@ -1664,9 +1663,6 @@ def _setup(options, lib=None): Returns a list of subcommands, a list of plugins, and a library instance. """ - # Configure the MusicBrainz API. - mb.configure() - config = _configure(options) plugins = _load_plugins(options, config) diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index 369a3cc73..08fb97f59 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -18,13 +18,14 @@ autotagger. Requires the pyacoustid library. import re from collections import defaultdict -from functools import partial +from functools import cached_property, partial import acoustid import confuse from beets import config, plugins, ui, util -from beets.autotag import hooks +from beets.autotag.hooks import Distance +from beetsplug.musicbrainz import MusicBrainzPlugin API_KEY = "1vOwZtEn" SCORE_THRESH = 0.5 @@ -182,11 +183,15 @@ class AcoustidPlugin(plugins.BeetsPlugin): self.register_listener("import_task_start", self.fingerprint_task) self.register_listener("import_task_apply", apply_acoustid_metadata) + @cached_property + def mb(self) -> MusicBrainzPlugin: + return MusicBrainzPlugin() + def fingerprint_task(self, task, session): return fingerprint_task(self._log, task, session) def track_distance(self, item, info): - dist = hooks.Distance() + dist = Distance() if item.path not in _matches or not info.track_id: # Match failed or no track ID. return dist @@ -198,7 +203,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): def candidates(self, items, artist, album, va_likely, extra_tags=None): albums = [] for relid in prefix(_all_releases(items), MAX_RELEASES): - album = hooks.album_for_mbid(relid) + album = self.mb.album_for_id(relid) if album: albums.append(album) @@ -212,7 +217,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): recording_ids, _ = _matches[item.path] tracks = [] for recording_id in prefix(recording_ids, MAX_RECORDINGS): - track = hooks.track_for_mbid(recording_id) + track = self.mb.track_for_id(recording_id) if track: tracks.append(track) self._log.debug("acoustid item candidates: {0}", len(tracks)) diff --git a/test/plugins/test_musicbrainz.py b/test/plugins/test_musicbrainz.py index 37b5c0fff..2d4821b3a 100644 --- a/test/plugins/test_musicbrainz.py +++ b/test/plugins/test_musicbrainz.py @@ -17,8 +17,8 @@ from unittest import mock from beets import config -from beets.autotag import mb from beets.test.helper import BeetsTestCase +from beetsplug import musicbrainz class MBAlbumInfoTest(BeetsTestCase): From de0958ca65dd00ce270ca0ed67f42ffc133a915a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 21 Apr 2025 23:09:16 +0100 Subject: [PATCH 053/728] Use candidate function from plugins instead of hooks --- beets/autotag/hooks.py | 25 +------------------------ beets/autotag/match.py | 14 ++++++++------ beetsplug/mbsync.py | 7 +++---- beetsplug/missing.py | 5 ++--- 4 files changed, 14 insertions(+), 37 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 530b5d7ad..88213b06a 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar from jellyfish import levenshtein_distance from unidecode import unidecode -from beets import config, logging, plugins +from beets import config, logging from beets.util import as_string, cached_classproperty if TYPE_CHECKING: @@ -590,26 +590,3 @@ class AlbumMatch(NamedTuple): class TrackMatch(NamedTuple): distance: Distance info: TrackInfo - - -# Aggregation of sources. - - -def album_for_id(*args, **kwargs) -> AlbumInfo | None: - """Get AlbumInfo object for the given ID string.""" - return plugins.album_for_id(*args, **kwargs) - - -def track_for_id(*args, **kwargs) -> TrackInfo | None: - """Get TrackInfo object for the given ID string.""" - return plugins.track_for_id(*args, **kwargs) - - -@plugins.notify_info_yielded("albuminfo_received") -def album_candidates(*args, **kwargs) -> Iterator[AlbumInfo]: - yield from plugins.candidates(*args, **kwargs) - - -@plugins.notify_info_yielded("trackinfo_received") -def item_candidates(*args, **kwargs) -> Iterator[TrackInfo]: - yield from plugins.item_candidates(*args, **kwargs) diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 5333b93fe..b3df1304f 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -356,7 +356,7 @@ def match_by_id(items: Iterable[Item]) -> AlbumInfo | None: return None # If all album IDs are equal, look up the album. log.debug("Searching for discovered album ID: {0}", first) - return hooks.album_for_id(first) + return plugins.album_for_id(first) def _recommendation( @@ -511,7 +511,7 @@ def tag_album( if search_ids: for search_id in search_ids: log.debug("Searching for album ID: {0}", search_id) - if info := hooks.album_for_id(search_id): + if info := plugins.album_for_id(search_id): _add_candidate(items, candidates, info) # Use existing metadata or text search. @@ -554,8 +554,8 @@ def tag_album( log.debug("Album might be VA: {0}", va_likely) # Get the results from the data sources. - for matched_candidate in hooks.album_candidates( - items, search_artist, search_album, va_likely, extra_tags + for matched_candidate in plugins.candidates( + items, search_artist, search_album, va_likely ): _add_candidate(items, candidates, matched_candidate) @@ -589,7 +589,7 @@ def tag_item( if trackids: for trackid in trackids: log.debug("Searching for track ID: {0}", trackid) - if info := hooks.track_for_id(trackid): + if info := plugins.track_for_id(trackid): dist = track_distance(item, info, incl_artist=True) candidates[info.track_id] = hooks.TrackMatch(dist, info) # If this is a good match, then don't keep searching. @@ -615,7 +615,9 @@ def tag_item( log.debug("Item search terms: {0} - {1}", search_artist, search_title) # Get and evaluate candidate metadata. - for track_info in hooks.item_candidates(item, search_artist, search_title): + for track_info in plugins.item_candidates( + item, search_artist, search_title + ): dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 2e62b7b7e..94870232c 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -16,8 +16,7 @@ from collections import defaultdict -from beets import autotag, library, ui, util -from beets.autotag import hooks +from beets import autotag, library, plugins, ui, util from beets.plugins import BeetsPlugin, apply_item_changes @@ -80,7 +79,7 @@ class MBSyncPlugin(BeetsPlugin): ) continue - if not (track_info := hooks.track_for_id(item.mb_trackid)): + if not (track_info := plugins.track_for_id(item.mb_trackid)): self._log.info( "Recording ID not found: {0.mb_trackid} for track {0}", item ) @@ -101,7 +100,7 @@ class MBSyncPlugin(BeetsPlugin): self._log.info("Skipping album with no mb_albumid: {}", album) continue - if not (album_info := hooks.album_for_id(album.mb_albumid)): + if not (album_info := plugins.album_for_id(album.mb_albumid)): self._log.info( "Release ID {0.mb_albumid} not found for album {0}", album ) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index ccaa65320..c4bbb83fd 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -21,8 +21,7 @@ from collections.abc import Iterator import musicbrainzngs from musicbrainzngs.musicbrainz import MusicBrainzError -from beets import config -from beets.autotag import hooks +from beets import config, plugins from beets.dbcore import types from beets.library import Album, Item, Library from beets.plugins import BeetsPlugin @@ -223,7 +222,7 @@ class MissingPlugin(BeetsPlugin): item_mbids = {x.mb_trackid for x in album.items()} # fetch missing items # TODO: Implement caching that without breaking other stuff - if album_info := hooks.album_for_id(album.mb_albumid): + if album_info := plugins.album_for_id(album.mb_albumid): for track_info in album_info.tracks: if track_info.track_id not in item_mbids: self._log.debug( From a538a3a15030eae0a45b0dcc0bf1230604b6b65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 17 Feb 2025 20:46:39 +0000 Subject: [PATCH 054/728] musicbrainz: move handling of extra tags to musicbrainz plugin --- beets/autotag/match.py | 6 ------ beetsplug/musicbrainz.py | 19 ++++++++++--------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/beets/autotag/match.py b/beets/autotag/match.py index b3df1304f..843e3d696 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -539,12 +539,6 @@ def tag_album( search_artist, search_album = cur_artist, cur_album log.debug("Search terms: {0} - {1}", search_artist, search_album) - extra_tags = None - if config["musicbrainz"]["extra_tags"]: - tag_list = config["musicbrainz"]["extra_tags"].get() - extra_tags = {k: v for (k, v) in likelies.items() if k in tag_list} - log.debug("Additional search terms: {0}", extra_tags) - # Is this album likely to be a "various artist" release? va_likely = ( (not consensus["artist"]) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index fdbdf1598..6f991fae5 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -783,15 +783,16 @@ class MusicBrainzPlugin(BeetsPlugin): if track_count := len(items): criteria["tracks"] = str(track_count) - # Additional search cues from existing metadata. - if extra_tags: - for tag, value in extra_tags.items(): - key = FIELDS_TO_MB_KEYS[tag] - value = str(value).lower().strip() - if key == "catno": - value = value.replace(" ", "") - if value: - criteria[key] = value + if self.config["extra_tags"]: + tag_list = self.config["extra_tags"].get() + self._log.debug("Additional search terms: {0}", tag_list) + for tag, value in tag_list.items(): + if key := FIELDS_TO_MB_KEYS.get(tag): + value = str(value).lower().strip() + if key == "catno": + value = value.replace(" ", "") + if value: + criteria[key] = value # Abort if we have no search terms. if not any(criteria.values()): From 5df857674ce1a8486125e80f8e83918880eaadf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 17 Feb 2025 20:52:43 +0000 Subject: [PATCH 055/728] plugins: add types and documentation to metadata backends methods and functions --- beets/plugins.py | 155 ++++++++++++++++++++--------------------------- 1 file changed, 65 insertions(+), 90 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index d33458825..560324fa1 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -22,7 +22,6 @@ import re import sys import traceback from collections import defaultdict -from collections.abc import Iterable from functools import wraps from typing import ( TYPE_CHECKING, @@ -46,6 +45,8 @@ else: if TYPE_CHECKING: + from collections.abc import Iterator + from confuse import ConfigView from beets.autotag import AlbumInfo, Distance, TrackInfo @@ -64,6 +65,11 @@ if TYPE_CHECKING: AnyModel = TypeVar("AnyModel", Album, Item) + P = ParamSpec("P") + Ret = TypeVar("Ret", bound=Any) + Listener = Callable[..., None] + IterF = Callable[P, Iterator[Ret]] + PLUGIN_NAMESPACE = "beetsplug" @@ -74,11 +80,6 @@ LASTFM_KEY = "2dc3914abf35f0d9c92d97d8f8e42b43" log = logging.getLogger("beets") -P = ParamSpec("P") -Ret = TypeVar("Ret", bound=Any) -Listener = Callable[..., None] - - class PluginConflictError(Exception): """Indicates that the services provided by one plugin conflict with those of another. @@ -242,22 +243,29 @@ class BeetsPlugin: album: str, va_likely: bool, extra_tags: dict[str, Any] | None = None, - ) -> Sequence[AlbumInfo]: - """Should return a sequence of AlbumInfo objects that match the - album whose items are provided. + ) -> Iterator[AlbumInfo]: + """Return :py:class:`AlbumInfo` candidates that match the given album. + + :param items: List of items in the album + :param artist: Album artist + :param album: Album name + :param va_likely: Whether the album is likely to be by various artists + :param extra_tags: is a an optional dictionary of extra tags to search. + Only relevant to :py:class:`MusicBrainzPlugin` autotagger and can be + ignored by other plugins """ - return () + yield from () def item_candidates( - self, - item: Item, - artist: str, - title: str, - ) -> Sequence[TrackInfo]: - """Should return a sequence of TrackInfo objects that match the - item provided. + self, item: Item, artist: str, title: str + ) -> Iterator[TrackInfo]: + """Return :py:class:`TrackInfo` candidates that match the given track. + + :param item: Track item + :param artist: Track artist + :param title: Track title """ - return () + yield from () def album_for_id(self, album_id: str) -> AlbumInfo | None: """Return an AlbumInfo object or None if no matching release was @@ -475,24 +483,37 @@ def album_distance( return dist -def candidates( - items: list[Item], - artist: str, - album: str, - va_likely: bool, - extra_tags: dict[str, Any] | None = None, -) -> Iterable[AlbumInfo]: - """Gets MusicBrainz candidates for an album from each plugin.""" - for plugin in find_plugins(): - yield from plugin.candidates( - items, artist, album, va_likely, extra_tags - ) +def notify_info_yielded(event: str) -> Callable[[IterF[P, Ret]], IterF[P, Ret]]: + """Makes a generator send the event 'event' every time it yields. + This decorator is supposed to decorate a generator, but any function + returning an iterable should work. + Each yielded value is passed to plugins using the 'info' parameter of + 'send'. + """ + + def decorator(generator: IterF[P, Ret]) -> IterF[P, Ret]: + def decorated(*args: P.args, **kwargs: P.kwargs) -> Iterator[Ret]: + for v in generator(*args, **kwargs): + send(event, info=v) + yield v + + return decorated + + return decorator -def item_candidates(item: Item, artist: str, title: str) -> Iterable[TrackInfo]: - """Gets MusicBrainz candidates for an item from the plugins.""" +@notify_info_yielded("albuminfo_received") +def candidates(*args, **kwargs) -> Iterator[AlbumInfo]: + """Return matching album candidates from all plugins.""" for plugin in find_plugins(): - yield from plugin.item_candidates(item, artist, title) + yield from plugin.candidates(*args, **kwargs) + + +@notify_info_yielded("trackinfo_received") +def item_candidates(*args, **kwargs) -> Iterator[TrackInfo]: + """Return matching track candidates from all plugins.""" + for plugin in find_plugins(): + yield from plugin.item_candidates(*args, **kwargs) def album_for_id(_id: str) -> AlbumInfo | None: @@ -695,32 +716,6 @@ def sanitize_pairs( return res -IterF = Callable[P, Iterable[Ret]] - - -def notify_info_yielded( - event: str, -) -> Callable[[IterF[P, Ret]], IterF[P, Ret]]: - """Makes a generator send the event 'event' every time it yields. - This decorator is supposed to decorate a generator, but any function - returning an iterable should work. - Each yielded value is passed to plugins using the 'info' parameter of - 'send'. - """ - - def decorator( - generator: IterF[P, Ret], - ) -> IterF[P, Ret]: - def decorated(*args: P.args, **kwargs: P.kwargs) -> Iterable[Ret]: - for v in generator(*args, **kwargs): - send(event, info=v) - yield v - - return decorated - - return decorator - - def get_distance( config: ConfigView, data_source: str, info: AlbumInfo | TrackInfo ) -> Distance: @@ -828,9 +823,7 @@ class MetadataSourcePlugin(Generic[R], BeetsPlugin, metaclass=abc.ABCMeta): raise NotImplementedError @abc.abstractmethod - def track_for_id( - self, track_id: str | None = None, track_data: R | None = None - ) -> TrackInfo | None: + def track_for_id(self, track_id: str) -> TrackInfo | None: raise NotImplementedError @staticmethod @@ -911,40 +904,22 @@ class MetadataSourcePlugin(Generic[R], BeetsPlugin, metaclass=abc.ABCMeta): album: str, va_likely: bool, extra_tags: dict[str, Any] | None = None, - ) -> Sequence[AlbumInfo]: - """Returns a list of AlbumInfo objects for Search API results - matching an ``album`` and ``artist`` (if not various). - - :param items: List of items comprised by an album to be matched. - :param artist: The artist of the album to be matched. - :param album: The name of the album to be matched. - :param va_likely: True if the album to be matched likely has - Various Artists. - """ + ) -> Iterator[AlbumInfo]: query_filters = {"album": album} if not va_likely: query_filters["artist"] = artist - results = self._search_api(query_type="album", filters=query_filters) - albums = [self.album_for_id(album_id=r["id"]) for r in results] - return [a for a in albums if a is not None] + for result in self._search_api("album", query_filters): + if info := self.album_for_id(result["id"]): + yield info def item_candidates( self, item: Item, artist: str, title: str - ) -> Sequence[TrackInfo]: - """Returns a list of TrackInfo objects for Search API results - matching ``title`` and ``artist``. - - :param item: Singleton item to be matched. - :param artist: The artist of the track to be matched. - :param title: The title of the track to be matched. - """ - track_responses = self._search_api( - query_type="track", keywords=title, filters={"artist": artist} - ) - - tracks = [self.track_for_id(track_data=r) for r in track_responses] - - return [t for t in tracks if t is not None] + ) -> Iterator[TrackInfo]: + for result in self._search_api( + "track", {"artist": artist}, keywords=title + ): + if info := self.track_for_id(result["id"]): + yield info def album_distance( self, From 4fc9f0c3d623ffdd3b63429090b19107891fdd98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 17 Feb 2025 21:01:48 +0000 Subject: [PATCH 056/728] Centralize AutotagStub test setup into AutotagImportTestCase --- beets/test/helper.py | 15 +++++-- test/plugins/test_edit.py | 12 ++--- test/plugins/test_importadded.py | 10 +---- test/plugins/test_mbsubmit.py | 16 ++----- test/test_importer.py | 76 +++++++------------------------- test/test_plugins.py | 4 +- 6 files changed, 40 insertions(+), 93 deletions(-) diff --git a/beets/test/helper.py b/beets/test/helper.py index 85ea6bcf7..ca9b8f406 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -35,6 +35,7 @@ import subprocess import sys import unittest from contextlib import contextmanager +from dataclasses import dataclass from enum import Enum from functools import cached_property from io import StringIO @@ -774,6 +775,7 @@ class TerminalImportMixin(ImportHelper): ) +@dataclass class AutotagStub: """Stub out MusicBrainz album and track matcher and control what the autotagger returns. @@ -784,11 +786,9 @@ class AutotagStub: GOOD = "GOOD" BAD = "BAD" MISSING = "MISSING" - """Generate an album match for all but one track - """ + matching: str length = 2 - matching = IDENT def install(self): self.mb_match_album = autotag.mb.match_album @@ -877,6 +877,15 @@ class AutotagStub: ) +class AutotagImportTestCase(ImportTestCase): + matching = AutotagStub.IDENT + + def setUp(self): + super().setUp() + self.matcher = AutotagStub(self.matching).install() + self.addCleanup(self.matcher.restore) + + class FetchImageHelper: """Helper mixin for mocking requests when fetching images with remote art sources. diff --git a/test/plugins/test_edit.py b/test/plugins/test_edit.py index 2d557d623..278e04b9e 100644 --- a/test/plugins/test_edit.py +++ b/test/plugins/test_edit.py @@ -19,9 +19,9 @@ from beets.dbcore.query import TrueQuery from beets.library import Item from beets.test import _common from beets.test.helper import ( + AutotagImportTestCase, AutotagStub, BeetsTestCase, - ImportTestCase, PluginMixin, TerminalImportMixin, control_stdin, @@ -316,10 +316,12 @@ class EditCommandTest(EditMixin, BeetsTestCase): @_common.slow_test() class EditDuringImporterTestCase( - EditMixin, TerminalImportMixin, ImportTestCase + EditMixin, TerminalImportMixin, AutotagImportTestCase ): """TODO""" + matching = AutotagStub.GOOD + IGNORED = ["added", "album_id", "id", "mtime", "path"] def setUp(self): @@ -327,12 +329,6 @@ class EditDuringImporterTestCase( # Create some mediafiles, and store them for comparison. self.prepare_album_for_import(1) self.items_orig = [Item.from_path(f.path) for f in self.import_media] - self.matcher = AutotagStub().install() - self.matcher.matching = AutotagStub.GOOD - - def tearDown(self): - super().tearDown() - self.matcher.restore() @_common.slow_test() diff --git a/test/plugins/test_importadded.py b/test/plugins/test_importadded.py index 608afb399..c3c7065d6 100644 --- a/test/plugins/test_importadded.py +++ b/test/plugins/test_importadded.py @@ -20,7 +20,7 @@ import os import pytest from beets import importer -from beets.test.helper import AutotagStub, ImportTestCase, PluginMixin +from beets.test.helper import AutotagImportTestCase, PluginMixin from beets.util import displayable_path, syspath from beetsplug.importadded import ImportAddedPlugin @@ -41,7 +41,7 @@ def modify_mtimes(paths, offset=-60000): os.utime(syspath(path), (mstat.st_atime, mstat.st_mtime + offset * i)) -class ImportAddedTest(PluginMixin, ImportTestCase): +class ImportAddedTest(PluginMixin, AutotagImportTestCase): # The minimum mtime of the files to be imported plugin = "importadded" min_mtime = None @@ -56,15 +56,9 @@ class ImportAddedTest(PluginMixin, ImportTestCase): self.min_mtime = min( os.path.getmtime(mfile.path) for mfile in self.import_media ) - self.matcher = AutotagStub().install() - self.matcher.matching = AutotagStub.IDENT self.importer = self.setup_importer() self.importer.add_choice(importer.action.APPLY) - def tearDown(self): - super().tearDown() - self.matcher.restore() - def find_media_file(self, item): """Find the pre-import MediaFile for an Item""" for m in self.import_media: diff --git a/test/plugins/test_mbsubmit.py b/test/plugins/test_mbsubmit.py index 04b1b736e..712c90866 100644 --- a/test/plugins/test_mbsubmit.py +++ b/test/plugins/test_mbsubmit.py @@ -14,8 +14,7 @@ from beets.test.helper import ( - AutotagStub, - ImportTestCase, + AutotagImportTestCase, PluginMixin, TerminalImportMixin, capture_stdout, @@ -23,23 +22,18 @@ from beets.test.helper import ( ) -class MBSubmitPluginTest(PluginMixin, TerminalImportMixin, ImportTestCase): +class MBSubmitPluginTest( + PluginMixin, TerminalImportMixin, AutotagImportTestCase +): plugin = "mbsubmit" def setUp(self): super().setUp() self.prepare_album_for_import(2) self.setup_importer() - self.matcher = AutotagStub().install() - - def tearDown(self): - super().tearDown() - self.matcher.restore() def test_print_tracks_output(self): """Test the output of the "print tracks" choice.""" - self.matcher.matching = AutotagStub.BAD - with capture_stdout() as output: with control_stdin("\n".join(["p", "s"])): # Print tracks; Skip @@ -55,8 +49,6 @@ class MBSubmitPluginTest(PluginMixin, TerminalImportMixin, ImportTestCase): def test_print_tracks_output_as_tracks(self): """Test the output of the "print tracks" choice, as singletons.""" - self.matcher.matching = AutotagStub.BAD - with capture_stdout() as output: with control_stdin("\n".join(["t", "s", "p", "s"])): # as Tracks; Skip; Print tracks; Skip diff --git a/test/test_importer.py b/test/test_importer.py index a28b646cf..804266116 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -39,6 +39,7 @@ from beets.test import _common from beets.test.helper import ( NEEDS_REFLINK, AsIsImporterMixin, + AutotagImportTestCase, AutotagStub, BeetsTestCase, ImportTestCase, @@ -306,7 +307,7 @@ class ImportPasswordRarTest(ImportZipTest): return os.path.join(_common.RSRC, b"password.rar") -class ImportSingletonTest(ImportTestCase): +class ImportSingletonTest(AutotagImportTestCase): """Test ``APPLY`` and ``ASIS`` choices for an import session with singletons config set to True. """ @@ -315,11 +316,6 @@ class ImportSingletonTest(ImportTestCase): super().setUp() self.prepare_album_for_import(1) self.importer = self.setup_singleton_importer() - self.matcher = AutotagStub().install() - - def tearDown(self): - super().tearDown() - self.matcher.restore() def test_apply_asis_adds_track(self): assert self.lib.items().get() is None @@ -432,19 +428,13 @@ class ImportSingletonTest(ImportTestCase): assert item.disc == disc -class ImportTest(ImportTestCase): +class ImportTest(AutotagImportTestCase): """Test APPLY, ASIS and SKIP choices.""" def setUp(self): super().setUp() self.prepare_album_for_import(1) self.setup_importer() - self.matcher = AutotagStub().install() - self.matcher.matching = AutotagStub.IDENT - - def tearDown(self): - super().tearDown() - self.matcher.restore() def test_apply_asis_adds_album(self): assert self.lib.albums().get() is None @@ -639,18 +629,13 @@ class ImportTest(ImportTestCase): assert item.disc == disc -class ImportTracksTest(ImportTestCase): +class ImportTracksTest(AutotagImportTestCase): """Test TRACKS and APPLY choice.""" def setUp(self): super().setUp() self.prepare_album_for_import(1) self.setup_importer() - self.matcher = AutotagStub().install() - - def tearDown(self): - super().tearDown() - self.matcher.restore() def test_apply_tracks_adds_singleton_track(self): assert self.lib.items().get() is None @@ -673,18 +658,13 @@ class ImportTracksTest(ImportTestCase): self.assert_file_in_lib(b"singletons", b"Applied Track 1.mp3") -class ImportCompilationTest(ImportTestCase): +class ImportCompilationTest(AutotagImportTestCase): """Test ASIS import of a folder containing tracks with different artists.""" def setUp(self): super().setUp() self.prepare_album_for_import(3) self.setup_importer() - self.matcher = AutotagStub().install() - - def tearDown(self): - super().tearDown() - self.matcher.restore() def test_asis_homogenous_sets_albumartist(self): self.importer.add_choice(importer.action.ASIS) @@ -783,21 +763,16 @@ class ImportCompilationTest(ImportTestCase): assert asserted_multi_artists_1 -class ImportExistingTest(ImportTestCase): +class ImportExistingTest(AutotagImportTestCase): """Test importing files that are already in the library directory.""" def setUp(self): super().setUp() self.prepare_album_for_import(1) - self.matcher = AutotagStub().install() self.reimporter = self.setup_importer(import_dir=self.libdir) self.importer = self.setup_importer() - def tearDown(self): - super().tearDown() - self.matcher.restore() - def test_does_not_duplicate_item(self): self.importer.run() assert len(self.lib.items()) == 1 @@ -904,12 +879,12 @@ class ImportExistingTest(ImportTestCase): self.assertNotExists(self.import_media[0].path) -class GroupAlbumsImportTest(ImportTestCase): +class GroupAlbumsImportTest(AutotagImportTestCase): + matching = AutotagStub.NONE + def setUp(self): super().setUp() self.prepare_album_for_import(3) - self.matcher = AutotagStub().install() - self.matcher.matching = AutotagStub.NONE self.setup_importer() # Split tracks into two albums and use both as-is @@ -917,10 +892,6 @@ class GroupAlbumsImportTest(ImportTestCase): self.importer.add_choice(importer.action.ASIS) self.importer.add_choice(importer.action.ASIS) - def tearDown(self): - super().tearDown() - self.matcher.restore() - def test_add_album_for_different_artist_and_different_album(self): self.import_media[0].artist = "Artist B" self.import_media[0].album = "Album B" @@ -976,17 +947,13 @@ class GlobalGroupAlbumsImportTest(GroupAlbumsImportTest): config["import"]["group_albums"] = True -class ChooseCandidateTest(ImportTestCase): +class ChooseCandidateTest(AutotagImportTestCase): + matching = AutotagStub.BAD + def setUp(self): super().setUp() self.prepare_album_for_import(1) self.setup_importer() - self.matcher = AutotagStub().install() - self.matcher.matching = AutotagStub.BAD - - def tearDown(self): - super().tearDown() - self.matcher.restore() def test_choose_first_candidate(self): self.importer.add_choice(1) @@ -1566,7 +1533,7 @@ class MultiDiscAlbumsInDirTest(BeetsTestCase): assert len(items) == 3 -class ReimportTest(ImportTestCase): +class ReimportTest(AutotagImportTestCase): """Test "re-imports", in which the autotagging machinery is used for music that's already in the library. @@ -1575,6 +1542,8 @@ class ReimportTest(ImportTestCase): attributes and the added date. """ + matching = AutotagStub.GOOD + def setUp(self): super().setUp() @@ -1589,14 +1558,6 @@ class ReimportTest(ImportTestCase): item.added = 4747.0 item.store() - # Set up an import pipeline with a "good" match. - self.matcher = AutotagStub().install() - self.matcher.matching = AutotagStub.GOOD - - def tearDown(self): - super().tearDown() - self.matcher.restore() - def _setup_session(self, singletons=False): self.setup_importer(import_dir=self.libdir, singletons=singletons) self.importer.add_choice(importer.action.APPLY) @@ -1677,22 +1638,17 @@ class ReimportTest(ImportTestCase): assert self._album().data_source == "match_source" -class ImportPretendTest(ImportTestCase): +class ImportPretendTest(AutotagImportTestCase): """Test the pretend commandline option""" def setUp(self): super().setUp() - self.matcher = AutotagStub().install() self.io.install() self.album_track_path = self.prepare_album_for_import(1)[0] self.single_path = self.prepare_track_for_import(2, self.import_path) self.album_path = self.album_track_path.parent - def tearDown(self): - super().tearDown() - self.matcher.restore() - def __run(self, importer): with capture_log() as logs: importer.run() diff --git a/test/test_plugins.py b/test/test_plugins.py index d273de698..4564f6690 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -347,7 +347,8 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): def setUp(self): super().setUp() self.setup_importer() - self.matcher = AutotagStub().install() + self.matcher = AutotagStub(AutotagStub.IDENT).install() + self.addCleanup(self.matcher.restore) # keep track of ui.input_option() calls self.input_options_patcher = patch( "beets.ui.input_options", side_effect=ui.input_options @@ -357,7 +358,6 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): def tearDown(self): super().tearDown() self.input_options_patcher.stop() - self.matcher.restore() def test_plugin_choices_in_ui_input_options_album(self): """Test the presence of plugin choices on the prompt (album).""" From bef0bcbaa6b6f6e97663a34d2f5d6141918dd7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 17 Feb 2025 21:16:56 +0000 Subject: [PATCH 057/728] musicbrainz: synchronise plugin import path --- test/plugins/test_albumtypes.py | 2 +- test/plugins/test_musicbrainz.py | 146 +++++++++++++++++-------------- 2 files changed, 80 insertions(+), 68 deletions(-) diff --git a/test/plugins/test_albumtypes.py b/test/plugins/test_albumtypes.py index 8be1ff011..0a9d53349 100644 --- a/test/plugins/test_albumtypes.py +++ b/test/plugins/test_albumtypes.py @@ -16,9 +16,9 @@ from collections.abc import Sequence -from beets.autotag.mb import VARIOUS_ARTISTS_ID from beets.test.helper import PluginTestCase from beetsplug.albumtypes import AlbumTypesPlugin +from beetsplug.musicbrainz import VARIOUS_ARTISTS_ID class AlbumTypesPluginTest(PluginTestCase): diff --git a/test/plugins/test_musicbrainz.py b/test/plugins/test_musicbrainz.py index 2d4821b3a..b8640c870 100644 --- a/test/plugins/test_musicbrainz.py +++ b/test/plugins/test_musicbrainz.py @@ -21,7 +21,13 @@ from beets.test.helper import BeetsTestCase from beetsplug import musicbrainz -class MBAlbumInfoTest(BeetsTestCase): +class MusicBrainzTestCase(BeetsTestCase): + def setUp(self): + super().setUp() + self.mb = musicbrainz.MusicBrainzPlugin() + + +class MBAlbumInfoTest(MusicBrainzTestCase): def _make_release( self, date_str="2009", @@ -210,7 +216,7 @@ class MBAlbumInfoTest(BeetsTestCase): def test_parse_release_with_year(self): release = self._make_release("1984") - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.album == "ALBUM TITLE" assert d.album_id == "ALBUM ID" assert d.artist == "ARTIST NAME" @@ -221,12 +227,12 @@ class MBAlbumInfoTest(BeetsTestCase): def test_parse_release_type(self): release = self._make_release("1984") - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.albumtype == "album" def test_parse_release_full_date(self): release = self._make_release("1987-03-31") - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.original_year == 1987 assert d.original_month == 3 assert d.original_day == 31 @@ -238,7 +244,7 @@ class MBAlbumInfoTest(BeetsTestCase): ] release = self._make_release(tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) t = d.tracks assert len(t) == 2 assert t[0].title == "TITLE ONE" @@ -255,7 +261,7 @@ class MBAlbumInfoTest(BeetsTestCase): ] release = self._make_release(tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) t = d.tracks assert t[0].medium_index == 1 assert t[0].index == 1 @@ -269,7 +275,7 @@ class MBAlbumInfoTest(BeetsTestCase): ] release = self._make_release(tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.mediums == 1 t = d.tracks assert t[0].medium == 1 @@ -296,7 +302,7 @@ class MBAlbumInfoTest(BeetsTestCase): } ) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.mediums == 2 t = d.tracks assert t[0].medium == 1 @@ -308,79 +314,81 @@ class MBAlbumInfoTest(BeetsTestCase): def test_parse_release_year_month_only(self): release = self._make_release("1987-03") - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.original_year == 1987 assert d.original_month == 3 def test_no_durations(self): tracks = [self._make_track("TITLE", "ID", None)] release = self._make_release(tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.tracks[0].length is None def test_track_length_overrides_recording_length(self): tracks = [self._make_track("TITLE", "ID", 1.0 * 1000.0)] release = self._make_release(tracks=tracks, track_length=2.0 * 1000.0) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.tracks[0].length == 2.0 def test_no_release_date(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert not d.original_year assert not d.original_month assert not d.original_day def test_various_artists_defaults_false(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert not d.va def test_detect_various_artists(self): release = self._make_release(None) - release["artist-credit"][0]["artist"]["id"] = mb.VARIOUS_ARTISTS_ID - d = mb.album_info(release) + release["artist-credit"][0]["artist"]["id"] = ( + musicbrainz.VARIOUS_ARTISTS_ID + ) + d = self.mb.album_info(release) assert d.va def test_parse_artist_sort_name(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.artist_sort == "ARTIST SORT NAME" def test_parse_releasegroupid(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.releasegroup_id == "RELEASE GROUP ID" def test_parse_asin(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.asin == "ALBUM ASIN" def test_parse_catalognum(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.catalognum == "CATALOG NUMBER" def test_parse_textrepr(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.script == "SCRIPT" assert d.language == "LANGUAGE" def test_parse_country(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.country == "COUNTRY" def test_parse_status(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.albumstatus == "STATUS" def test_parse_barcode(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.barcode == "BARCODE" def test_parse_media(self): @@ -389,12 +397,12 @@ class MBAlbumInfoTest(BeetsTestCase): self._make_track("TITLE TWO", "ID TWO", 200.0 * 1000.0), ] release = self._make_release(None, tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.media == "FORMAT" def test_parse_disambig(self): release = self._make_release(None) - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.albumdisambig == "R_DISAMBIGUATION" assert d.releasegroupdisambig == "RG_DISAMBIGUATION" @@ -404,7 +412,7 @@ class MBAlbumInfoTest(BeetsTestCase): self._make_track("TITLE TWO", "ID TWO", 200.0 * 1000.0), ] release = self._make_release(None, tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) t = d.tracks assert t[0].disctitle == "MEDIUM TITLE" assert t[1].disctitle == "MEDIUM TITLE" @@ -412,13 +420,13 @@ class MBAlbumInfoTest(BeetsTestCase): def test_missing_language(self): release = self._make_release(None) del release["text-representation"]["language"] - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.language is None def test_parse_recording_artist(self): tracks = [self._make_track("a", "b", 1, True)] release = self._make_release(None, tracks=tracks) - track = mb.album_info(release).tracks[0] + track = self.mb.album_info(release).tracks[0] assert track.artist == "RECORDING ARTIST NAME" assert track.artist_id == "RECORDING ARTIST ID" assert track.artist_sort == "RECORDING ARTIST SORT NAME" @@ -427,7 +435,7 @@ class MBAlbumInfoTest(BeetsTestCase): def test_parse_recording_artist_multi(self): tracks = [self._make_track("a", "b", 1, True, multi_artist_credit=True)] release = self._make_release(None, tracks=tracks) - track = mb.album_info(release).tracks[0] + track = self.mb.album_info(release).tracks[0] assert track.artist == "RECORDING ARTIST NAME & RECORDING ARTIST 2 NAME" assert track.artist_id == "RECORDING ARTIST ID" assert ( @@ -459,7 +467,7 @@ class MBAlbumInfoTest(BeetsTestCase): def test_track_artist_overrides_recording_artist(self): tracks = [self._make_track("a", "b", 1, True)] release = self._make_release(None, tracks=tracks, track_artist=True) - track = mb.album_info(release).tracks[0] + track = self.mb.album_info(release).tracks[0] assert track.artist == "TRACK ARTIST NAME" assert track.artist_id == "TRACK ARTIST ID" assert track.artist_sort == "TRACK ARTIST SORT NAME" @@ -470,7 +478,7 @@ class MBAlbumInfoTest(BeetsTestCase): release = self._make_release( None, tracks=tracks, track_artist=True, multi_artist_credit=True ) - track = mb.album_info(release).tracks[0] + track = self.mb.album_info(release).tracks[0] assert track.artist == "TRACK ARTIST NAME & TRACK ARTIST 2 NAME" assert track.artist_id == "TRACK ARTIST ID" assert ( @@ -495,12 +503,12 @@ class MBAlbumInfoTest(BeetsTestCase): def test_parse_recording_remixer(self): tracks = [self._make_track("a", "b", 1, remixer=True)] release = self._make_release(None, tracks=tracks) - track = mb.album_info(release).tracks[0] + track = self.mb.album_info(release).tracks[0] assert track.remixer == "RECORDING REMIXER ARTIST NAME" def test_data_source(self): release = self._make_release() - d = mb.album_info(release) + d = self.mb.album_info(release) assert d.data_source == "MusicBrainz" def test_ignored_media(self): @@ -510,7 +518,7 @@ class MBAlbumInfoTest(BeetsTestCase): self._make_track("TITLE TWO", "ID TWO", 200.0 * 1000.0), ] release = self._make_release(tracks=tracks, medium_format="IGNORED1") - d = mb.album_info(release) + d = self.mb.album_info(release) assert len(d.tracks) == 0 def test_no_ignored_media(self): @@ -520,7 +528,7 @@ class MBAlbumInfoTest(BeetsTestCase): self._make_track("TITLE TWO", "ID TWO", 200.0 * 1000.0), ] release = self._make_release(tracks=tracks, medium_format="NON-IGNORED") - d = mb.album_info(release) + d = self.mb.album_info(release) assert len(d.tracks) == 2 def test_skip_data_track(self): @@ -530,7 +538,7 @@ class MBAlbumInfoTest(BeetsTestCase): self._make_track("TITLE TWO", "ID TWO", 200.0 * 1000.0), ] release = self._make_release(tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert len(d.tracks) == 2 assert d.tracks[0].title == "TITLE ONE" assert d.tracks[1].title == "TITLE TWO" @@ -546,7 +554,7 @@ class MBAlbumInfoTest(BeetsTestCase): ) ] release = self._make_release(tracks=tracks, data_tracks=data_tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert len(d.tracks) == 2 assert d.tracks[0].title == "TITLE ONE" assert d.tracks[1].title == "TITLE TWO" @@ -563,7 +571,7 @@ class MBAlbumInfoTest(BeetsTestCase): ) ] release = self._make_release(tracks=tracks, data_tracks=data_tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert len(d.tracks) == 3 assert d.tracks[0].title == "TITLE ONE" assert d.tracks[1].title == "TITLE TWO" @@ -578,7 +586,7 @@ class MBAlbumInfoTest(BeetsTestCase): self._make_track("TITLE TWO", "ID TWO", 200.0 * 1000.0), ] release = self._make_release(tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert len(d.tracks) == 2 assert d.tracks[0].title == "TITLE ONE" assert d.tracks[1].title == "TITLE TWO" @@ -594,7 +602,7 @@ class MBAlbumInfoTest(BeetsTestCase): ) ] release = self._make_release(tracks=tracks, data_tracks=data_tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert len(d.tracks) == 2 assert d.tracks[0].title == "TITLE ONE" assert d.tracks[1].title == "TITLE TWO" @@ -610,7 +618,7 @@ class MBAlbumInfoTest(BeetsTestCase): self._make_track("TITLE TWO", "ID TWO", 200.0 * 1000.0), ] release = self._make_release(tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert len(d.tracks) == 3 assert d.tracks[0].title == "TITLE ONE" assert d.tracks[1].title == "TITLE VIDEO" @@ -629,7 +637,7 @@ class MBAlbumInfoTest(BeetsTestCase): ) ] release = self._make_release(tracks=tracks, data_tracks=data_tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) assert len(d.tracks) == 3 assert d.tracks[0].title == "TITLE ONE" assert d.tracks[1].title == "TITLE TWO" @@ -647,7 +655,7 @@ class MBAlbumInfoTest(BeetsTestCase): ] release = self._make_release(tracks=tracks) - d = mb.album_info(release) + d = self.mb.album_info(release) t = d.tracks assert len(t) == 2 assert t[0].trackdisambig is None @@ -657,18 +665,18 @@ class MBAlbumInfoTest(BeetsTestCase): class ParseIDTest(BeetsTestCase): def test_parse_id_correct(self): id_string = "28e32c71-1450-463e-92bf-e0a46446fc11" - out = mb._parse_id(id_string) + out = musicbrainz._parse_id(id_string) assert out == id_string def test_parse_id_non_id_returns_none(self): id_string = "blah blah" - out = mb._parse_id(id_string) + out = musicbrainz._parse_id(id_string) assert out is None def test_parse_id_url_finds_id(self): id_string = "28e32c71-1450-463e-92bf-e0a46446fc11" id_url = "https://musicbrainz.org/entity/%s" % id_string - out = mb._parse_id(id_url) + out = musicbrainz._parse_id(id_url) assert out == id_string @@ -696,24 +704,28 @@ class ArtistFlatteningTest(BeetsTestCase): def test_single_artist(self): credit = [self._credit_dict()] - a, s, c = mb._flatten_artist_credit(credit) + a, s, c = musicbrainz._flatten_artist_credit(credit) assert a == "NAME" assert s == "SORT" assert c == "CREDIT" - a, s, c = mb._multi_artist_credit(credit, include_join_phrase=False) + a, s, c = musicbrainz._multi_artist_credit( + credit, include_join_phrase=False + ) assert a == ["NAME"] assert s == ["SORT"] assert c == ["CREDIT"] def test_two_artists(self): credit = [self._credit_dict("a"), " AND ", self._credit_dict("b")] - a, s, c = mb._flatten_artist_credit(credit) + a, s, c = musicbrainz._flatten_artist_credit(credit) assert a == "NAMEa AND NAMEb" assert s == "SORTa AND SORTb" assert c == "CREDITa AND CREDITb" - a, s, c = mb._multi_artist_credit(credit, include_join_phrase=False) + a, s, c = musicbrainz._multi_artist_credit( + credit, include_join_phrase=False + ) assert a == ["NAMEa", "NAMEb"] assert s == ["SORTa", "SORTb"] assert c == ["CREDITa", "CREDITb"] @@ -730,36 +742,36 @@ class ArtistFlatteningTest(BeetsTestCase): # test no alias config["import"]["languages"] = [""] - flat = mb._flatten_artist_credit([credit_dict]) + flat = musicbrainz._flatten_artist_credit([credit_dict]) assert flat == ("NAME", "SORT", "CREDIT") # test en primary config["import"]["languages"] = ["en"] - flat = mb._flatten_artist_credit([credit_dict]) + flat = musicbrainz._flatten_artist_credit([credit_dict]) assert flat == ("ALIASen", "ALIASSORTen", "CREDIT") # test en_GB en primary config["import"]["languages"] = ["en_GB", "en"] - flat = mb._flatten_artist_credit([credit_dict]) + flat = musicbrainz._flatten_artist_credit([credit_dict]) assert flat == ("ALIASen_GB", "ALIASSORTen_GB", "CREDIT") # test en en_GB primary config["import"]["languages"] = ["en", "en_GB"] - flat = mb._flatten_artist_credit([credit_dict]) + flat = musicbrainz._flatten_artist_credit([credit_dict]) assert flat == ("ALIASen", "ALIASSORTen", "CREDIT") # test fr primary config["import"]["languages"] = ["fr"] - flat = mb._flatten_artist_credit([credit_dict]) + flat = musicbrainz._flatten_artist_credit([credit_dict]) assert flat == ("ALIASfr_P", "ALIASSORTfr_P", "CREDIT") # test for not matching non-primary config["import"]["languages"] = ["pt_BR", "fr"] - flat = mb._flatten_artist_credit([credit_dict]) + flat = musicbrainz._flatten_artist_credit([credit_dict]) assert flat == ("ALIASfr_P", "ALIASSORTfr_P", "CREDIT") -class MBLibraryTest(BeetsTestCase): +class MBLibraryTest(MusicBrainzTestCase): def test_match_track(self): with mock.patch("musicbrainzngs.search_recordings") as p: p.return_value = { @@ -771,13 +783,13 @@ class MBLibraryTest(BeetsTestCase): } ], } - ti = list(mb.match_track("hello", "there"))[0] + ti = list(self.mb.item_candidates(None, "hello", "there"))[0] p.assert_called_with(artist="hello", recording="there", limit=5) assert ti.title == "foo" assert ti.track_id == "bar" - def test_match_album(self): + def test_candidates(self): mbid = "d2a6f856-b553-40a0-ac54-a321e8e2da99" with mock.patch("musicbrainzngs.search_releases") as sp: sp.return_value = { @@ -824,7 +836,7 @@ class MBLibraryTest(BeetsTestCase): } } - ai = list(mb.match_album("hello", "there"))[0] + ai = list(self.mb.candidates([], "hello", "there", False))[0] sp.assert_called_with(artist="hello", release="there", limit=5) gp.assert_called_with(mbid, mock.ANY) @@ -833,13 +845,13 @@ class MBLibraryTest(BeetsTestCase): def test_match_track_empty(self): with mock.patch("musicbrainzngs.search_recordings") as p: - til = list(mb.match_track(" ", " ")) + til = list(self.mb.item_candidates(None, " ", " ")) assert not p.called assert til == [] - def test_match_album_empty(self): + def test_candidates_empty(self): with mock.patch("musicbrainzngs.search_releases") as p: - ail = list(mb.match_album(" ", " ")) + ail = list(self.mb.candidates([], " ", " ", False)) assert not p.called assert ail == [] @@ -927,7 +939,7 @@ class MBLibraryTest(BeetsTestCase): with mock.patch("musicbrainzngs.get_release_by_id") as gp: gp.side_effect = side_effect - album = mb.album_for_id("d2a6f856-b553-40a0-ac54-a321e8e2da02") + album = self.mb.album_for_id("d2a6f856-b553-40a0-ac54-a321e8e2da02") assert album.country == "COUNTRY" def test_pseudo_releases_with_empty_links(self): @@ -972,7 +984,7 @@ class MBLibraryTest(BeetsTestCase): with mock.patch("musicbrainzngs.get_release_by_id") as gp: gp.side_effect = side_effect - album = mb.album_for_id("d2a6f856-b553-40a0-ac54-a321e8e2da02") + album = self.mb.album_for_id("d2a6f856-b553-40a0-ac54-a321e8e2da02") assert album.country is None def test_pseudo_releases_without_links(self): @@ -1016,7 +1028,7 @@ class MBLibraryTest(BeetsTestCase): with mock.patch("musicbrainzngs.get_release_by_id") as gp: gp.side_effect = side_effect - album = mb.album_for_id("d2a6f856-b553-40a0-ac54-a321e8e2da02") + album = self.mb.album_for_id("d2a6f856-b553-40a0-ac54-a321e8e2da02") assert album.country is None def test_pseudo_releases_with_unsupported_links(self): @@ -1067,5 +1079,5 @@ class MBLibraryTest(BeetsTestCase): with mock.patch("musicbrainzngs.get_release_by_id") as gp: gp.side_effect = side_effect - album = mb.album_for_id("d2a6f856-b553-40a0-ac54-a321e8e2da02") + album = self.mb.album_for_id("d2a6f856-b553-40a0-ac54-a321e8e2da02") assert album.country is None From 0980c82959cf806bae5824450635a11a1cdd9925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 17 Feb 2025 23:29:36 +0000 Subject: [PATCH 058/728] musicbrainz: update patches --- beets/test/helper.py | 43 ++++----- beets/ui/commands.py | 2 +- test/test_importer.py | 215 ++++++++++++++---------------------------- 3 files changed, 88 insertions(+), 172 deletions(-) diff --git a/beets/test/helper.py b/beets/test/helper.py index ca9b8f406..f83f274d5 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -49,7 +49,7 @@ from mediafile import Image, MediaFile import beets import beets.plugins -from beets import autotag, importer, logging, util +from beets import importer, logging, util from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.importer import ImportSession from beets.library import Album, Item, Library @@ -791,40 +791,37 @@ class AutotagStub: length = 2 def install(self): - self.mb_match_album = autotag.mb.match_album - self.mb_match_track = autotag.mb.match_track - self.mb_album_for_id = autotag.mb.album_for_id - self.mb_track_for_id = autotag.mb.track_for_id - - autotag.mb.match_album = self.match_album - autotag.mb.match_track = self.match_track - autotag.mb.album_for_id = self.album_for_id - autotag.mb.track_for_id = self.track_for_id + self.patchers = [ + patch("beets.plugins.album_for_id", lambda *_: None), + patch("beets.plugins.track_for_id", lambda *_: None), + patch("beets.plugins.candidates", self.candidates), + patch("beets.plugins.item_candidates", self.item_candidates), + ] + for p in self.patchers: + p.start() return self def restore(self): - autotag.mb.match_album = self.mb_match_album - autotag.mb.match_track = self.mb_match_track - autotag.mb.album_for_id = self.mb_album_for_id - autotag.mb.track_for_id = self.mb_track_for_id + for p in self.patchers: + p.stop() - def match_album(self, albumartist, album, tracks, extra_tags): + def candidates(self, items, artist, album, va_likely, extra_tags=None): if self.matching == self.IDENT: - yield self._make_album_match(albumartist, album, tracks) + yield self._make_album_match(artist, album, len(items)) elif self.matching == self.GOOD: for i in range(self.length): - yield self._make_album_match(albumartist, album, tracks, i) + yield self._make_album_match(artist, album, len(items), i) elif self.matching == self.BAD: for i in range(self.length): - yield self._make_album_match(albumartist, album, tracks, i + 1) + yield self._make_album_match(artist, album, len(items), i + 1) elif self.matching == self.MISSING: - yield self._make_album_match(albumartist, album, tracks, missing=1) + yield self._make_album_match(artist, album, len(items), missing=1) - def match_track(self, artist, title): + def item_candidates(self, item, artist, title): yield TrackInfo( title=title.replace("Tag", "Applied"), track_id="trackid", @@ -834,12 +831,6 @@ class AutotagStub: index=0, ) - def album_for_id(self, mbid): - return None - - def track_for_id(self, mbid): - return None - def _make_track_match(self, artist, album, number): return TrackInfo( title="Applied Track %d" % number, diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 12ea4c94d..94693ea4e 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -363,7 +363,7 @@ class ChangeRepresentation: self.indent_header + f"Match ({dist_string(self.match.distance)}):" ) - if self.match.info.get("album"): + if isinstance(self.match.info, autotag.hooks.AlbumInfo): # Matching an album - print that artist_album_str = ( f"{self.match.info.artist}" + f" - {self.match.info.album}" diff --git a/test/test_importer.py b/test/test_importer.py index 804266116..dad30749b 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -1061,26 +1061,22 @@ class InferAlbumDataTest(BeetsTestCase): assert not self.items[0].comp -def match_album_mock(*args, **kwargs): +def album_candidates_mock(*args, **kwargs): """Create an AlbumInfo object for testing.""" - track_info = TrackInfo( - title="new title", - track_id="trackid", - index=0, - ) - album_info = AlbumInfo( + yield AlbumInfo( artist="artist", album="album", - tracks=[track_info], + tracks=[TrackInfo(title="new title", track_id="trackid", index=0)], album_id="albumid", artist_id="artistid", flex="flex", ) - return iter([album_info]) -@patch("beets.autotag.mb.match_album", Mock(side_effect=match_album_mock)) -class ImportDuplicateAlbumTest(ImportTestCase): +@patch("beets.plugins.candidates", Mock(side_effect=album_candidates_mock)) +class ImportDuplicateAlbumTest(PluginMixin, ImportTestCase): + plugin = "musicbrainz" + def setUp(self): super().setUp() @@ -1186,20 +1182,16 @@ class ImportDuplicateAlbumTest(ImportTestCase): return album -def match_track_mock(*args, **kwargs): - return iter( - [ - TrackInfo( - artist="artist", - title="title", - track_id="new trackid", - index=0, - ) - ] +def item_candidates_mock(*args, **kwargs): + yield TrackInfo( + artist="artist", + title="title", + track_id="new trackid", + index=0, ) -@patch("beets.autotag.mb.match_track", Mock(side_effect=match_track_mock)) +@patch("beets.plugins.item_candidates", Mock(side_effect=item_candidates_mock)) class ImportDuplicateSingletonTest(ImportTestCase): def setUp(self): super().setUp() @@ -1633,7 +1625,7 @@ class ReimportTest(AutotagImportTestCase): def test_reimported_album_not_preserves_flexattr(self): self._setup_session() - assert self._album().data_source == "original_source" + self.importer.run() assert self._album().data_source == "match_source" @@ -1657,6 +1649,7 @@ class ImportPretendTest(AutotagImportTestCase): assert len(self.lib.albums()) == 0 return [line for line in logs if not line.startswith("Sending event:")] + assert self._album().data_source == "original_source" def test_import_singletons_pretend(self): assert self.__run(self.setup_singleton_importer(pretend=True)) == [ @@ -1681,112 +1674,64 @@ class ImportPretendTest(AutotagImportTestCase): assert self.__run(importer) == [f"No files imported from {empty_path}"] -# Helpers for ImportMusicBrainzIdTest. +def mocked_get_album_by_id(id_): + """Return album candidate for the given id. - -def mocked_get_release_by_id( - id_, includes=[], release_status=[], release_type=[] -): - """Mimic musicbrainzngs.get_release_by_id, accepting only a restricted list - of MB ids (ID_RELEASE_0, ID_RELEASE_1). The returned dict differs only in - the release title and artist name, so that ID_RELEASE_0 is a closer match - to the items created by ImportHelper.prepare_album_for_import().""" + The two albums differ only in the release title and artist name, so that + ID_RELEASE_0 is a closer match to the items created by + ImportHelper.prepare_album_for_import(). + """ # Map IDs to (release title, artist), so the distances are different. - releases = { - ImportMusicBrainzIdTest.ID_RELEASE_0: ("VALID_RELEASE_0", "TAG ARTIST"), - ImportMusicBrainzIdTest.ID_RELEASE_1: ( - "VALID_RELEASE_1", - "DISTANT_MATCH", - ), - } + album, artist = { + ImportIdTest.ID_RELEASE_0: ("VALID_RELEASE_0", "TAG ARTIST"), + ImportIdTest.ID_RELEASE_1: ("VALID_RELEASE_1", "DISTANT_MATCH"), + }[id_] - return { - "release": { - "title": releases[id_][0], - "id": id_, - "medium-list": [ - { - "track-list": [ - { - "id": "baz", - "recording": { - "title": "foo", - "id": "bar", - "length": 59, - }, - "position": 9, - "number": "A2", - } - ], - "position": 5, - } - ], - "artist-credit": [ - { - "artist": { - "name": releases[id_][1], - "id": "some-id", - }, - } - ], - "release-group": { - "id": "another-id", - }, - "status": "Official", - } - } + return AlbumInfo( + album_id=id_, + album=album, + artist_id="some-id", + artist=artist, + albumstatus="Official", + tracks=[ + TrackInfo( + track_id="bar", + title="foo", + artist_id="some-id", + artist=artist, + length=59, + index=9, + track_allt="A2", + ) + ], + ) -def mocked_get_recording_by_id( - id_, includes=[], release_status=[], release_type=[] -): - """Mimic musicbrainzngs.get_recording_by_id, accepting only a restricted - list of MB ids (ID_RECORDING_0, ID_RECORDING_1). The returned dict differs - only in the recording title and artist name, so that ID_RECORDING_0 is a - closer match to the items created by ImportHelper.prepare_album_for_import(). +def mocked_get_track_by_id(id_): + """Return track candidate for the given id. + + The two tracks differ only in the release title and artist name, so that + ID_RELEASE_0 is a closer match to the items created by + ImportHelper.prepare_album_for_import(). """ # Map IDs to (recording title, artist), so the distances are different. - releases = { - ImportMusicBrainzIdTest.ID_RECORDING_0: ( - "VALID_RECORDING_0", - "TAG ARTIST", - ), - ImportMusicBrainzIdTest.ID_RECORDING_1: ( - "VALID_RECORDING_1", - "DISTANT_MATCH", - ), - } + title, artist = { + ImportIdTest.ID_RECORDING_0: ("VALID_RECORDING_0", "TAG ARTIST"), + ImportIdTest.ID_RECORDING_1: ("VALID_RECORDING_1", "DISTANT_MATCH"), + }[id_] - return { - "recording": { - "title": releases[id_][0], - "id": id_, - "length": 59, - "artist-credit": [ - { - "artist": { - "name": releases[id_][1], - "id": "some-id", - }, - } - ], - } - } + return TrackInfo( + track_id=id_, + title=title, + artist_id="some-id", + artist=artist, + length=59, + ) -@patch( - "musicbrainzngs.get_recording_by_id", - Mock(side_effect=mocked_get_recording_by_id), -) -@patch( - "musicbrainzngs.get_release_by_id", - Mock(side_effect=mocked_get_release_by_id), -) -class ImportMusicBrainzIdTest(ImportTestCase): - """Test the --musicbrainzid argument.""" - - MB_RELEASE_PREFIX = "https://musicbrainz.org/release/" - MB_RECORDING_PREFIX = "https://musicbrainz.org/recording/" +@patch("beets.plugins.track_for_id", Mock(side_effect=mocked_get_track_by_id)) +@patch("beets.plugins.album_for_id", Mock(side_effect=mocked_get_album_by_id)) +class ImportIdTest(ImportTestCase): ID_RELEASE_0 = "00000000-0000-0000-0000-000000000000" ID_RELEASE_1 = "11111111-1111-1111-1111-111111111111" ID_RECORDING_0 = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" @@ -1797,21 +1742,14 @@ class ImportMusicBrainzIdTest(ImportTestCase): self.prepare_album_for_import(1) def test_one_mbid_one_album(self): - self.setup_importer( - search_ids=[self.MB_RELEASE_PREFIX + self.ID_RELEASE_0] - ) + self.setup_importer(search_ids=[self.ID_RELEASE_0]) self.importer.add_choice(importer.action.APPLY) self.importer.run() assert self.lib.albums().get().album == "VALID_RELEASE_0" def test_several_mbid_one_album(self): - self.setup_importer( - search_ids=[ - self.MB_RELEASE_PREFIX + self.ID_RELEASE_0, - self.MB_RELEASE_PREFIX + self.ID_RELEASE_1, - ] - ) + self.setup_importer(search_ids=[self.ID_RELEASE_0, self.ID_RELEASE_1]) self.importer.add_choice(2) # Pick the 2nd best match (release 1). self.importer.add_choice(importer.action.APPLY) @@ -1819,9 +1757,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): assert self.lib.albums().get().album == "VALID_RELEASE_1" def test_one_mbid_one_singleton(self): - self.setup_singleton_importer( - search_ids=[self.MB_RECORDING_PREFIX + self.ID_RECORDING_0] - ) + self.setup_singleton_importer(search_ids=[self.ID_RECORDING_0]) self.importer.add_choice(importer.action.APPLY) self.importer.run() @@ -1829,10 +1765,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): def test_several_mbid_one_singleton(self): self.setup_singleton_importer( - search_ids=[ - self.MB_RECORDING_PREFIX + self.ID_RECORDING_0, - self.MB_RECORDING_PREFIX + self.ID_RECORDING_1, - ] + search_ids=[self.ID_RECORDING_0, self.ID_RECORDING_1] ) self.importer.add_choice(2) # Pick the 2nd best match (recording 1). @@ -1845,11 +1778,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): task = importer.ImportTask( paths=self.import_dir, toppath="top path", items=[_common.item()] ) - task.search_ids = [ - self.MB_RELEASE_PREFIX + self.ID_RELEASE_0, - self.MB_RELEASE_PREFIX + self.ID_RELEASE_1, - "an invalid and discarded id", - ] + task.search_ids = [self.ID_RELEASE_0, self.ID_RELEASE_1] task.lookup_candidates() assert {"VALID_RELEASE_0", "VALID_RELEASE_1"} == { @@ -1861,11 +1790,7 @@ class ImportMusicBrainzIdTest(ImportTestCase): task = importer.SingletonImportTask( toppath="top path", item=_common.item() ) - task.search_ids = [ - self.MB_RECORDING_PREFIX + self.ID_RECORDING_0, - self.MB_RECORDING_PREFIX + self.ID_RECORDING_1, - "an invalid and discarded id", - ] + task.search_ids = [self.ID_RECORDING_0, self.ID_RECORDING_1] task.lookup_candidates() assert {"VALID_RECORDING_0", "VALID_RECORDING_1"} == { From df56bfeec9c65b4bae7be69dba64e9dc783b1a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sat, 17 May 2025 00:02:19 +0100 Subject: [PATCH 059/728] Move musicbrainz docs to a separate file --- docs/plugins/musicbrainz.rst | 108 ++++++++++++++++++++++++++++++++++ docs/reference/config.rst | 109 ----------------------------------- 2 files changed, 108 insertions(+), 109 deletions(-) create mode 100644 docs/plugins/musicbrainz.rst diff --git a/docs/plugins/musicbrainz.rst b/docs/plugins/musicbrainz.rst new file mode 100644 index 000000000..780970bb4 --- /dev/null +++ b/docs/plugins/musicbrainz.rst @@ -0,0 +1,108 @@ +.. _musicbrainz-config: + +MusicBrainz Options +------------------- + +You can instruct beets to use `your own MusicBrainz database`_ instead of +the `main server`_. Use the ``host``, ``https`` and ``ratelimit`` options +under a ``musicbrainz:`` header, like so:: + + musicbrainz: + host: localhost:5000 + https: no + ratelimit: 100 + +The ``host`` key, of course, controls the Web server hostname (and port, +optionally) that will be contacted by beets (default: musicbrainz.org). +The ``https`` key makes the client use HTTPS instead of HTTP. This setting applies +only to custom servers. The official MusicBrainz server always uses HTTPS. (Default: no.) +The server must have search indices enabled (see `Building search indexes`_). + +The ``ratelimit`` option, an integer, controls the number of Web service requests +per second (default: 1). **Do not change the rate limit setting** if you're +using the main MusicBrainz server---on this public server, you're `limited`_ +to one request per second. + +.. _your own MusicBrainz database: https://musicbrainz.org/doc/MusicBrainz_Server/Setup +.. _main server: https://musicbrainz.org/ +.. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting +.. _Building search indexes: https://musicbrainz.org/doc/Development/Search_server_setup + +.. _musicbrainz.enabled: + +enabled +~~~~~~~ + +This option allows you to disable using MusicBrainz as a metadata source. This applies +if you use plugins that fetch data from alternative sources and should make the import +process quicker. + +Default: ``yes``. + +.. _searchlimit: + +searchlimit +~~~~~~~~~~~ + +The number of matches returned when sending search queries to the +MusicBrainz server. + +Default: ``5``. + +.. _extra_tags: + +extra_tags +~~~~~~~~~~ + +By default, beets will use only the artist, album, and track count to query +MusicBrainz. Additional tags to be queried can be supplied with the +``extra_tags`` setting. For example:: + + musicbrainz: + extra_tags: [year, catalognum, country, media, label] + +This setting should improve the autotagger results if the metadata with the +given tags match the metadata returned by MusicBrainz. + +Note that the only tags supported by this setting are the ones listed in the +above example. + +Default: ``[]`` + +.. _genres: + +genres +~~~~~~ + +Use MusicBrainz genre tags to populate (and replace if it's already set) the +``genre`` tag. This will make it a list of all the genres tagged for the +release and the release-group on MusicBrainz, separated by "; " and sorted by +the total number of votes. +Default: ``no`` + +.. _musicbrainz.external_ids: + +external_ids +~~~~~~~~~~~~ + +Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz +importer to look for links to related metadata sources. If such a link is +available the release ID will be extracted from the URL provided and imported +to the beets library:: + + musicbrainz: + external_ids: + discogs: yes + spotify: yes + bandcamp: yes + beatport: yes + deezer: yes + tidal: yes + + +The library fields of the corresponding :ref:`autotagger_extensions` are used +to save the data (``discogs_albumid``, ``bandcamp_album_id``, +``spotify_album_id``, ``beatport_album_id``, ``deezer_album_id``, +``tidal_album_id``). On re-imports existing data will be overwritten. + +The default of all options is ``no``. diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 234185e79..fc7e7f36f 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -874,115 +874,6 @@ This feature is currently supported by the :doc:`/plugins/discogs` and the Default: ``yes``. -.. _musicbrainz-config: - -MusicBrainz Options -------------------- - -You can instruct beets to use `your own MusicBrainz database`_ instead of -the `main server`_. Use the ``host``, ``https`` and ``ratelimit`` options -under a ``musicbrainz:`` header, like so:: - - musicbrainz: - host: localhost:5000 - https: no - ratelimit: 100 - -The ``host`` key, of course, controls the Web server hostname (and port, -optionally) that will be contacted by beets (default: musicbrainz.org). -The ``https`` key makes the client use HTTPS instead of HTTP. This setting applies -only to custom servers. The official MusicBrainz server always uses HTTPS. (Default: no.) -The server must have search indices enabled (see `Building search indexes`_). - -The ``ratelimit`` option, an integer, controls the number of Web service requests -per second (default: 1). **Do not change the rate limit setting** if you're -using the main MusicBrainz server---on this public server, you're `limited`_ -to one request per second. - -.. _your own MusicBrainz database: https://musicbrainz.org/doc/MusicBrainz_Server/Setup -.. _main server: https://musicbrainz.org/ -.. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting -.. _Building search indexes: https://musicbrainz.org/doc/Development/Search_server_setup - -.. _musicbrainz.enabled: - -enabled -~~~~~~~ - -This option allows you to disable using MusicBrainz as a metadata source. This applies -if you use plugins that fetch data from alternative sources and should make the import -process quicker. - -Default: ``yes``. - -.. _searchlimit: - -searchlimit -~~~~~~~~~~~ - -The number of matches returned when sending search queries to the -MusicBrainz server. - -Default: ``5``. - -.. _extra_tags: - -extra_tags -~~~~~~~~~~ - -By default, beets will use only the artist, album, and track count to query -MusicBrainz. Additional tags to be queried can be supplied with the -``extra_tags`` setting. For example:: - - musicbrainz: - extra_tags: [year, catalognum, country, media, label] - -This setting should improve the autotagger results if the metadata with the -given tags match the metadata returned by MusicBrainz. - -Note that the only tags supported by this setting are the ones listed in the -above example. - -Default: ``[]`` - -.. _genres: - -genres -~~~~~~ - -Use MusicBrainz genre tags to populate (and replace if it's already set) the -``genre`` tag. This will make it a list of all the genres tagged for the -release and the release-group on MusicBrainz, separated by "; " and sorted by -the total number of votes. -Default: ``no`` - -.. _musicbrainz.external_ids: - -external_ids -~~~~~~~~~~~~ - -Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz -importer to look for links to related metadata sources. If such a link is -available the release ID will be extracted from the URL provided and imported -to the beets library:: - - musicbrainz: - external_ids: - discogs: yes - spotify: yes - bandcamp: yes - beatport: yes - deezer: yes - tidal: yes - - -The library fields of the corresponding :ref:`autotagger_extensions` are used -to save the data (``discogs_albumid``, ``bandcamp_album_id``, -``spotify_album_id``, ``beatport_album_id``, ``deezer_album_id``, -``tidal_album_id``). On re-imports existing data will be overwritten. - -The default of all options is ``no``. - .. _match-config: Autotagger Matching Options From 736d7d5fd0fea2247717cb756817af9b004dd2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sat, 17 May 2025 00:03:37 +0100 Subject: [PATCH 060/728] Make musicbrainz docs follow the typical style --- docs/plugins/musicbrainz.rst | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/docs/plugins/musicbrainz.rst b/docs/plugins/musicbrainz.rst index 780970bb4..c4c9ab1e9 100644 --- a/docs/plugins/musicbrainz.rst +++ b/docs/plugins/musicbrainz.rst @@ -1,11 +1,27 @@ +MusicBrainz Plugin +================== + +The ``musicbrainz`` plugin extends the autotagger's search capabilities to +include matches from the `MusicBrainz`_ database. + +.. _MusicBrainz: https://musicbrainz.org/ + +Installation +------------ + +To use the ``musicbrainz`` plugin, enable it in your configuration (see +:ref:`using-plugins`) + .. _musicbrainz-config: -MusicBrainz Options -------------------- +Configuration +------------- You can instruct beets to use `your own MusicBrainz database`_ instead of the `main server`_. Use the ``host``, ``https`` and ``ratelimit`` options -under a ``musicbrainz:`` header, like so:: +under a ``musicbrainz:`` header, like so + +.. code-block:: yaml musicbrainz: host: localhost:5000 @@ -56,7 +72,9 @@ extra_tags By default, beets will use only the artist, album, and track count to query MusicBrainz. Additional tags to be queried can be supplied with the -``extra_tags`` setting. For example:: +``extra_tags`` setting. For example + +.. code-block:: yaml musicbrainz: extra_tags: [year, catalognum, country, media, label] @@ -88,7 +106,9 @@ external_ids Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz importer to look for links to related metadata sources. If such a link is available the release ID will be extracted from the URL provided and imported -to the beets library:: +to the beets library + +.. code-block:: yaml musicbrainz: external_ids: From 7ff73d974755aa7b71dde31c11c34f2911158931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 2 Mar 2025 22:51:52 +0000 Subject: [PATCH 061/728] musicbrainz: set default config in the code --- beets/config_default.yaml | 19 ++----------------- beetsplug/musicbrainz.py | 18 ++++++++++++++++++ docs/plugins/musicbrainz.rst | 22 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/beets/config_default.yaml b/beets/config_default.yaml index c5cebd441..d1329f494 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -6,7 +6,8 @@ statefile: state.pickle # --------------- Plugins --------------- -plugins: [] +plugins: [musicbrainz] + pluginpath: [] # --------------- Import --------------- @@ -163,22 +164,6 @@ sort_case_insensitive: yes overwrite_null: album: [] track: [] -musicbrainz: - enabled: yes - host: musicbrainz.org - https: no - ratelimit: 1 - ratelimit_interval: 1.0 - searchlimit: 5 - extra_tags: [] - genres: no - external_ids: - discogs: no - bandcamp: no - spotify: no - deezer: no - beatport: no - tidal: no match: strong_rec_thresh: 0.04 diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index 6f991fae5..508537704 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -383,6 +383,24 @@ class MusicBrainzPlugin(BeetsPlugin): from the beets configuration. This should be called at startup. """ super().__init__() + self.config.add( + { + "host": "musicbrainz.org", + "https": False, + "ratelimit": 1, + "ratelimit_interval": 1, + "searchlimit": 5, + "genres": False, + "external_ids": { + "discogs": False, + "bandcamp": False, + "spotify": False, + "deezer": False, + "tidal": False, + }, + "extra_tags": {}, + }, + ) hostname = self.config["host"].as_str() https = self.config["https"].get(bool) # Only call set_hostname when a custom server is configured. Since diff --git a/docs/plugins/musicbrainz.rst b/docs/plugins/musicbrainz.rst index c4c9ab1e9..c624ae57e 100644 --- a/docs/plugins/musicbrainz.rst +++ b/docs/plugins/musicbrainz.rst @@ -17,6 +17,28 @@ To use the ``musicbrainz`` plugin, enable it in your configuration (see Configuration ------------- +Default +^^^^^^^ + +.. code-block:: yaml + + musicbrainz: + host: musicbrainz.org + https: no + ratelimit: 1 + ratelimit_interval: 1.0 + searchlimit: 5 + extra_tags: [] + genres: no + external_ids: + discogs: no + bandcamp: no + spotify: no + deezer: no + beatport: no + tidal: no + + You can instruct beets to use `your own MusicBrainz database`_ instead of the `main server`_. Use the ``host``, ``https`` and ``ratelimit`` options under a ``musicbrainz:`` header, like so From 33bed79a13c36e2b4fa59c134da65c6b57f5d6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Mon, 21 Apr 2025 20:01:31 +0100 Subject: [PATCH 062/728] Move scrub test to a separate file --- test/plugins/test_scrub.py | 37 ++++++++++++++++++++++++++++++ test/test_importer.py | 47 -------------------------------------- 2 files changed, 37 insertions(+), 47 deletions(-) create mode 100644 test/plugins/test_scrub.py diff --git a/test/plugins/test_scrub.py b/test/plugins/test_scrub.py new file mode 100644 index 000000000..129d91a22 --- /dev/null +++ b/test/plugins/test_scrub.py @@ -0,0 +1,37 @@ +import os + +from mediafile import MediaFile + +from beets.test.helper import AsIsImporterMixin, ImportTestCase, PluginMixin + + +class ScrubbedImportTest(AsIsImporterMixin, PluginMixin, ImportTestCase): + db_on_disk = True + plugin = "scrub" + + def test_tags_not_scrubbed(self): + with self.configure_plugin({"auto": False}): + self.run_asis_importer(write=True) + + for item in self.lib.items(): + imported_file = MediaFile(os.path.join(item.path)) + assert imported_file.artist == "Tag Artist" + assert imported_file.album == "Tag Album" + + def test_tags_restored(self): + with self.configure_plugin({"auto": True}): + self.run_asis_importer(write=True) + + for item in self.lib.items(): + imported_file = MediaFile(os.path.join(item.path)) + assert imported_file.artist == "Tag Artist" + assert imported_file.album == "Tag Album" + + def test_tags_not_restored(self): + with self.configure_plugin({"auto": True}): + self.run_asis_importer(write=False) + + for item in self.lib.items(): + imported_file = MediaFile(os.path.join(item.path)) + assert imported_file.artist is None + assert imported_file.album is None diff --git a/test/test_importer.py b/test/test_importer.py index dad30749b..2b353653f 100644 --- a/test/test_importer.py +++ b/test/test_importer.py @@ -50,53 +50,6 @@ from beets.test.helper import ( from beets.util import bytestring_path, displayable_path, syspath -class ScrubbedImportTest(AsIsImporterMixin, PluginMixin, ImportTestCase): - db_on_disk = True - plugin = "scrub" - - def test_tags_not_scrubbed(self): - config["plugins"] = ["scrub"] - config["scrub"]["auto"] = False - config["import"]["write"] = True - for mediafile in self.import_media: - assert mediafile.artist == "Tag Artist" - assert mediafile.album == "Tag Album" - self.run_asis_importer() - for item in self.lib.items(): - imported_file = os.path.join(item.path) - imported_file = MediaFile(imported_file) - assert imported_file.artist == "Tag Artist" - assert imported_file.album == "Tag Album" - - def test_tags_restored(self): - config["plugins"] = ["scrub"] - config["scrub"]["auto"] = True - config["import"]["write"] = True - for mediafile in self.import_media: - assert mediafile.artist == "Tag Artist" - assert mediafile.album == "Tag Album" - self.run_asis_importer() - for item in self.lib.items(): - imported_file = os.path.join(item.path) - imported_file = MediaFile(imported_file) - assert imported_file.artist == "Tag Artist" - assert imported_file.album == "Tag Album" - - def test_tags_not_restored(self): - config["plugins"] = ["scrub"] - config["scrub"]["auto"] = True - config["import"]["write"] = False - for mediafile in self.import_media: - assert mediafile.artist == "Tag Artist" - assert mediafile.album == "Tag Album" - self.run_asis_importer() - for item in self.lib.items(): - imported_file = os.path.join(item.path) - imported_file = MediaFile(imported_file) - assert imported_file.artist is None - assert imported_file.album is None - - @_common.slow_test() class NonAutotaggedImportTest(AsIsImporterMixin, ImportTestCase): db_on_disk = True From 874fb3da7b66bbbfae885c3c18e423b7d38e812c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Tue, 22 Apr 2025 13:14:28 +0100 Subject: [PATCH 063/728] Add changelog note about musicbrainz --- docs/changelog.rst | 6 ++++++ docs/reference/config.rst | 2 ++ 2 files changed, 8 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a18f195eb..aef033c56 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -36,6 +36,12 @@ been dropped. New features: +* :doc:`plugins/musicbrainz`: The MusicBrainz autotagger has been moved to + a separate plugin. The default :ref:`plugins-config` includes `musicbrainz`, + but if you've customized your `plugins` list in your configuration, you'll + need to explicitly add `musicbrainz` to continue using this functionality. + :bug:`2686` + :bug:`4605` * :doc:`plugins/lastgenre`: The new configuration option, ``keep_existing``, provides more fine-grained control over how pre-populated genre tags are handled. The ``force`` option now behaves in a more conventional manner. diff --git a/docs/reference/config.rst b/docs/reference/config.rst index fc7e7f36f..7e93b00ff 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -58,6 +58,8 @@ directory The directory to which files will be copied/moved when adding them to the library. Defaults to a folder called ``Music`` in your home directory. +.. _plugins-config: + plugins ~~~~~~~ From e981fb1aea53daa367c09eaa068026346e8225e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Tue, 22 Apr 2025 15:12:59 +0100 Subject: [PATCH 064/728] Deprecate musicbrainz.enabled configuration --- beets/ui/__init__.py | 18 +++++++++++++++--- docs/changelog.rst | 1 + docs/plugins/musicbrainz.rst | 3 +++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index fef2ae0c5..a6f615b45 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -17,6 +17,8 @@ interface. To invoke the CLI, just call beets.ui.main(). The actual CLI commands are implemented in the ui.commands module. """ +from __future__ import annotations + import errno import optparse import os.path @@ -27,7 +29,7 @@ import sys import textwrap import traceback from difflib import SequenceMatcher -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable import confuse @@ -37,6 +39,9 @@ from beets.dbcore import query as db_query from beets.util import as_string from beets.util.functemplate import template +if TYPE_CHECKING: + from types import ModuleType + # On Windows platforms, use colorama to support "ANSI" terminal colors. if sys.platform == "win32": try: @@ -569,7 +574,7 @@ COLOR_NAMES = [ "text_diff_removed", "text_diff_changed", ] -COLORS = None +COLORS: dict[str, list[str]] | None = None def _colorize(color, text): @@ -1622,7 +1627,9 @@ optparse.Option.ALWAYS_TYPED_ACTIONS += ("callback",) # The main entry point and bootstrapping. -def _load_plugins(options, config): +def _load_plugins( + options: optparse.Values, config: confuse.LazyConfig +) -> ModuleType: """Load the plugins specified on the command line or in the configuration.""" paths = config["pluginpath"].as_str_seq(split=False) paths = [util.normpath(p) for p in paths] @@ -1647,6 +1654,11 @@ def _load_plugins(options, config): ) else: plugin_list = config["plugins"].as_str_seq() + # TODO: Remove in v2.4 or v3 + if "musicbrainz" in config and config["musicbrainz"].get().get( + "enabled" + ): + plugin_list.append("musicbrainz") # Exclude any plugins that were specified on the command line if options.exclude is not None: diff --git a/docs/changelog.rst b/docs/changelog.rst index aef033c56..0f84f2473 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -40,6 +40,7 @@ New features: a separate plugin. The default :ref:`plugins-config` includes `musicbrainz`, but if you've customized your `plugins` list in your configuration, you'll need to explicitly add `musicbrainz` to continue using this functionality. + Configuration option `musicbrainz.enabled` has thus been deprecated. :bug:`2686` :bug:`4605` * :doc:`plugins/lastgenre`: The new configuration option, ``keep_existing``, diff --git a/docs/plugins/musicbrainz.rst b/docs/plugins/musicbrainz.rst index c624ae57e..ef10be66d 100644 --- a/docs/plugins/musicbrainz.rst +++ b/docs/plugins/musicbrainz.rst @@ -71,6 +71,9 @@ to one request per second. enabled ~~~~~~~ +.. deprecated:: 2.3 + Add `musicbrainz` to the `plugins` list instead. + This option allows you to disable using MusicBrainz as a metadata source. This applies if you use plugins that fetch data from alternative sources and should make the import process quicker. From 3f1d11707849f09a41ac5efe812a5ef2d26d3d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Tue, 22 Apr 2025 13:33:04 +0100 Subject: [PATCH 065/728] Add musicbrainz to plugins docs --- docs/plugins/index.rst | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index bd137291e..82fa94281 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -13,7 +13,9 @@ Using Plugins ------------- To use one of the plugins included with beets (see the rest of this page for a -list), just use the ``plugins`` option in your :doc:`config.yaml </reference/config>` file, like so:: +list), just use the ``plugins`` option in your :doc:`config.yaml </reference/config>` file: + +.. code-block:: sh plugins: inline convert web @@ -21,7 +23,9 @@ The value for ``plugins`` can be a space-separated list of plugin names or a YAML list like ``[foo, bar]``. You can see which plugins are currently enabled by typing ``beet version``. -Each plugin has its own set of options that can be defined in a section bearing its name:: +Each plugin has its own set of options that can be defined in a section bearing its name: + +.. code-block:: yaml plugins: inline convert web @@ -30,10 +34,11 @@ Each plugin has its own set of options that can be defined in a section bearing Some plugins have special dependencies that you'll need to install. The documentation page for each plugin will list them in the setup instructions. -For some, you can use ``pip``'s "extras" feature to install the dependencies, -like this:: +For some, you can use ``pip``'s "extras" feature to install the dependencies: - pip install beets[fetchart,lyrics,lastgenre] +.. code-block:: sh + + pip install "beets[fetchart,lyrics,lastgenre]" .. _metadata-source-plugin-configuration: @@ -48,7 +53,9 @@ plugins share the following configuration option: Default: ``0.5``. For example, to equally consider matches from Discogs and MusicBrainz add the -following to your configuration:: +following to your configuration: + +.. code-block:: yaml plugins: discogs @@ -111,6 +118,7 @@ following to your configuration:: missing mpdstats mpdupdate + musicbrainz parentwork permissions play @@ -142,21 +150,26 @@ Autotagger Extensions Use acoustic fingerprinting to identify audio files with missing or incorrect metadata. -:doc:`discogs <discogs>` - Search for releases in the `Discogs`_ database. - -:doc:`spotify <spotify>` - Search for releases in the `Spotify`_ database. - :doc:`deezer <deezer>` Search for releases in the `Deezer`_ database. +:doc:`discogs <discogs>` + Search for releases in the `Discogs`_ database. + :doc:`fromfilename <fromfilename>` Guess metadata for untagged tracks from their filenames. -.. _Discogs: https://www.discogs.com/ +:doc:`musicbrainz <musicbrainz>` + Search for releases in the `MusicBrainz`_ database. + +:doc:`spotify <spotify>` + Search for releases in the `Spotify`_ database. + + +.. _Deezer: https://www.deezer.com +.. _Discogs: https://www.discogs.com +.. _MusicBrainz: https://www.musicbrainz.com .. _Spotify: https://www.spotify.com -.. _Deezer: https://www.deezer.com/ Metadata -------- From 78462245b7360a6a7043ffdfee1ccef348ffefd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sun, 4 May 2025 14:35:30 +0100 Subject: [PATCH 066/728] Use wraps for notify_info_yielded decorator --- beets/plugins.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index 560324fa1..06af40c26 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -491,13 +491,14 @@ def notify_info_yielded(event: str) -> Callable[[IterF[P, Ret]], IterF[P, Ret]]: 'send'. """ - def decorator(generator: IterF[P, Ret]) -> IterF[P, Ret]: - def decorated(*args: P.args, **kwargs: P.kwargs) -> Iterator[Ret]: - for v in generator(*args, **kwargs): + def decorator(func: IterF[P, Ret]) -> IterF[P, Ret]: + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> Iterator[Ret]: + for v in func(*args, **kwargs): send(event, info=v) yield v - return decorated + return wrapper return decorator @@ -694,7 +695,7 @@ def sanitize_pairs( ... ) [('foo', 'baz'), ('foo', 'bar'), ('key', 'value'), ('foo', 'foobar')] """ - pairs_all: list[tuple[str, str]] = list(pairs_all) + pairs_all = list(pairs_all) seen: set[tuple[str, str]] = set() others = [x for x in pairs_all if x not in pairs] res: list[tuple[str, str]] = [] From f1dc75f7431987e6f17eb8cbf0d610c3b2180183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Wed, 7 May 2025 22:46:58 +0100 Subject: [PATCH 067/728] Fix types in all edited files --- beets/autotag/hooks.py | 8 ++++---- beets/autotag/match.py | 4 ++-- beets/plugins.py | 23 ++++++++--------------- beets/test/helper.py | 18 +++++++++--------- beets/util/id_extractors.py | 17 ++++++++++++++--- beetsplug/musicbrainz.py | 30 ++++++++++++++++-------------- 6 files changed, 53 insertions(+), 47 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 88213b06a..641a6cb4f 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -55,7 +55,7 @@ class AttrDict(dict[str, V]): return id(self) -class AlbumInfo(AttrDict): +class AlbumInfo(AttrDict[Any]): """Describes a canonical release that may be used to match a release in the library. Consists of these data members: @@ -165,7 +165,7 @@ class AlbumInfo(AttrDict): return dupe -class TrackInfo(AttrDict): +class TrackInfo(AttrDict[Any]): """Describes a canonical track present on a release. Appears as part of an AlbumInfo's ``tracks`` list. Consists of these data members: @@ -356,8 +356,8 @@ class Distance: for each individual penalty. """ - def __init__(self): - self._penalties = {} + def __init__(self) -> None: + self._penalties: dict[str, list[float]] = {} self.tracks: dict[TrackInfo, Distance] = {} @cached_classproperty diff --git a/beets/autotag/match.py b/beets/autotag/match.py index 843e3d696..91a315de0 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -604,8 +604,8 @@ def tag_item( return Proposal([], Recommendation.none) # Search terms. - if not (search_artist and search_title): - search_artist, search_title = item.artist, item.title + search_artist = search_artist or item.artist + search_title = search_title or item.title log.debug("Item search terms: {0} - {1}", search_artist, search_title) # Get and evaluate candidate metadata. diff --git a/beets/plugins.py b/beets/plugins.py index 06af40c26..8751e11ad 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -51,10 +51,12 @@ if TYPE_CHECKING: from beets.autotag import AlbumInfo, Distance, TrackInfo from beets.dbcore import Query - from beets.dbcore.db import FieldQueryType, SQLiteType + from beets.dbcore.db import FieldQueryType + from beets.dbcore.types import Type from beets.importer import ImportSession, ImportTask from beets.library import Album, Item, Library from beets.ui import Subcommand + from beets.util.id_extractors import RegexDict # TYPE_CHECKING guard is needed for any derived type # which uses an import from `beets.library` and `beets.imported` @@ -225,7 +227,7 @@ class BeetsPlugin: def album_distance( self, - items: list[Item], + items: Sequence[Item], album_info: AlbumInfo, mapping: dict[Item, TrackInfo], ) -> Distance: @@ -430,10 +432,10 @@ def queries() -> dict[str, type[Query]]: return out -def types(model_cls: type[AnyModel]) -> dict[str, type[SQLiteType]]: +def types(model_cls: type[AnyModel]) -> dict[str, Type]: # Gives us `item_types` and `album_types` attr_name = f"{model_cls.__name__.lower()}_types" - types: dict[str, type[SQLiteType]] = {} + types: dict[str, Type] = {} for plugin in find_plugins(): plugin_types = getattr(plugin, attr_name, {}) for field in plugin_types: @@ -470,7 +472,7 @@ def track_distance(item: Item, info: TrackInfo) -> Distance: def album_distance( - items: list[Item], + items: Sequence[Item], album_info: AlbumInfo, mapping: dict[Item, TrackInfo], ) -> Distance: @@ -768,15 +770,6 @@ class Response(TypedDict): id: str -class RegexDict(TypedDict): - """A dictionary containing a regex pattern and the number of the - match group. - """ - - pattern: str - match_group: int - - R = TypeVar("R", bound=Response) @@ -924,7 +917,7 @@ class MetadataSourcePlugin(Generic[R], BeetsPlugin, metaclass=abc.ABCMeta): def album_distance( self, - items: list[Item], + items: Sequence[Item], album_info: AlbumInfo, mapping: dict[Item, TrackInfo], ) -> Distance: diff --git a/beets/test/helper.py b/beets/test/helper.py index f83f274d5..bcfca4c82 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -448,6 +448,11 @@ class PluginMixin(ConfigMixin): plugin: ClassVar[str] preload_plugin: ClassVar[bool] = True + original_item_types = dict(Item._types) + original_album_types = dict(Album._types) + original_item_queries = dict(Item._queries) + original_album_queries = dict(Album._queries) + def setup_beets(self): super().setup_beets() if self.preload_plugin: @@ -471,13 +476,8 @@ class PluginMixin(ConfigMixin): # Take a backup of the original _types and _queries to restore # when unloading. - Item._original_types = dict(Item._types) - Album._original_types = dict(Album._types) Item._types.update(beets.plugins.types(Item)) Album._types.update(beets.plugins.types(Album)) - - Item._original_queries = dict(Item._queries) - Album._original_queries = dict(Album._queries) Item._queries.update(beets.plugins.named_queries(Item)) Album._queries.update(beets.plugins.named_queries(Album)) @@ -489,10 +489,10 @@ class PluginMixin(ConfigMixin): self.config["plugins"] = [] beets.plugins._classes = set() beets.plugins._instances = {} - Item._types = getattr(Item, "_original_types", {}) - Album._types = getattr(Album, "_original_types", {}) - Item._queries = getattr(Item, "_original_queries", {}) - Album._queries = getattr(Album, "_original_queries", {}) + Item._types = self.original_item_types + Album._types = self.original_album_types + Item._queries = self.original_item_queries + Album._queries = self.original_album_queries @contextmanager def configure_plugin(self, config: Any): diff --git a/beets/util/id_extractors.py b/beets/util/id_extractors.py index 04e9e94a7..4dbab087d 100644 --- a/beets/util/id_extractors.py +++ b/beets/util/id_extractors.py @@ -15,20 +15,31 @@ """Helpers around the extraction of album/track ID's from metadata sources.""" import re +from typing import TypedDict + + +class RegexDict(TypedDict): + """A dictionary containing a regex pattern and the number of the + match group. + """ + + pattern: str + match_group: int + # Spotify IDs consist of 22 alphanumeric characters # (zero-left-padded base62 representation of randomly generated UUID4) -spotify_id_regex = { +spotify_id_regex: RegexDict = { "pattern": r"(^|open\.spotify\.com/{}/)([0-9A-Za-z]{{22}})", "match_group": 2, } -deezer_id_regex = { +deezer_id_regex: RegexDict = { "pattern": r"(^|deezer\.com/)([a-z]*/)?({}/)?(\d+)", "match_group": 4, } -beatport_id_regex = { +beatport_id_regex: RegexDict = { "pattern": r"(^|beatport\.com/release/.+/)(\d+)$", "match_group": 2, } diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index 508537704..e1a640d84 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -41,6 +41,8 @@ if TYPE_CHECKING: from beets.library import Item + from ._typing import JSONDict + VARIOUS_ARTISTS_ID = "89ad4ac3-39f7-470e-963a-56509c546377" BASE_URL = "https://musicbrainz.org/" @@ -121,7 +123,7 @@ BROWSE_CHUNKSIZE = 100 BROWSE_MAXTRACKS = 500 -def _preferred_alias(aliases: list): +def _preferred_alias(aliases: list[JSONDict]): """Given an list of alias structures for an artist credit, select and return the user's preferred alias alias or None if no matching alias is found. @@ -130,7 +132,7 @@ def _preferred_alias(aliases: list): return # Only consider aliases that have locales set. - aliases = [a for a in aliases if "locale" in a] + valid_aliases = [a for a in aliases if "locale" in a] # Get any ignored alias types and lower case them to prevent case issues ignored_alias_types = config["import"]["ignored_alias_types"].as_str_seq() @@ -141,13 +143,13 @@ def _preferred_alias(aliases: list): # Find matching primary aliases for this locale that are not # being ignored matches = [] - for a in aliases: + for alias in valid_aliases: if ( - a["locale"] == locale - and "primary" in a - and a.get("type", "").lower() not in ignored_alias_types + alias["locale"] == locale + and "primary" in alias + and alias.get("type", "").lower() not in ignored_alias_types ): - matches.append(a) + matches.append(alias) # Skip to the next locale if we have no matches if not matches: @@ -157,7 +159,7 @@ def _preferred_alias(aliases: list): def _multi_artist_credit( - credit: list[dict], include_join_phrase: bool + credit: list[JSONDict], include_join_phrase: bool ) -> tuple[list[str], list[str], list[str]]: """Given a list representing an ``artist-credit`` block, accumulate data into a triple of joined artist name lists: canonical, sort, and @@ -209,7 +211,7 @@ def track_url(trackid: str) -> str: return urljoin(BASE_URL, "recording/" + trackid) -def _flatten_artist_credit(credit: list[dict]) -> tuple[str, str, str]: +def _flatten_artist_credit(credit: list[JSONDict]) -> tuple[str, str, str]: """Given a list representing an ``artist-credit`` block, flatten the data into a triple of joined artist name strings: canonical, sort, and credit. @@ -224,7 +226,7 @@ def _flatten_artist_credit(credit: list[dict]) -> tuple[str, str, str]: ) -def _artist_ids(credit: list[dict]) -> list[str]: +def _artist_ids(credit: list[JSONDict]) -> list[str]: """ Given a list representing an ``artist-credit``, return a list of artist IDs @@ -317,8 +319,8 @@ def _is_translation(r): def _find_actual_release_from_pseudo_release( - pseudo_rel: dict, -) -> dict | None: + pseudo_rel: JSONDict, +) -> JSONDict | None: try: relations = pseudo_rel["release"]["release-relation-list"] except KeyError: @@ -414,7 +416,7 @@ class MusicBrainzPlugin(BeetsPlugin): def track_info( self, - recording: dict, + recording: JSONDict, index: int | None = None, medium: int | None = None, medium_index: int | None = None, @@ -515,7 +517,7 @@ class MusicBrainzPlugin(BeetsPlugin): return info - def album_info(self, release: dict) -> beets.autotag.hooks.AlbumInfo: + def album_info(self, release: JSONDict) -> beets.autotag.hooks.AlbumInfo: """Takes a MusicBrainz release result dictionary and returns a beets AlbumInfo object containing the interesting data about that release. """ From 7d96334924b8a333bed16f8a5e7653ebffb6f074 Mon Sep 17 00:00:00 2001 From: Sebastian Mohr <sebastian@mohrenclan.de> Date: Sat, 17 May 2025 13:13:27 +0200 Subject: [PATCH 068/728] Added function move to git ignore --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 8848bf384..4703203ba 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -47,3 +47,5 @@ f36bc497c8c8f89004f3f6879908d3f0b25123e1 # 2025 # Fix formatting c490ac5810b70f3cf5fd8649669838e8fdb19f4d +# Importer restructure +9147577b2b19f43ca827e9650261a86fb0450cef \ No newline at end of file From b520981c9ce73e34f6fd13b790986502b8c1ef74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Thu, 8 May 2025 04:09:59 +0100 Subject: [PATCH 069/728] plugins: restructure id extraction --- beets/plugins.py | 28 ++----------- beets/util/id_extractors.py | 68 ++++++++++---------------------- beetsplug/beatport.py | 5 +-- beetsplug/deezer.py | 44 +++++++++------------ beetsplug/discogs.py | 6 +-- beetsplug/musicbrainz.py | 49 +++++------------------ beetsplug/spotify.py | 27 ++++++------- test/plugins/test_discogs.py | 32 --------------- test/plugins/test_musicbrainz.py | 18 --------- test/test_plugins.py | 64 ------------------------------ test/util/test_id_extractors.py | 34 ++++++++++++++++ 11 files changed, 103 insertions(+), 272 deletions(-) create mode 100644 test/util/test_id_extractors.py diff --git a/beets/plugins.py b/beets/plugins.py index 8751e11ad..26e70ed72 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -37,6 +37,7 @@ import mediafile import beets from beets import logging +from beets.util.id_extractors import extract_release_id if sys.version_info >= (3, 10): from typing import ParamSpec @@ -56,7 +57,6 @@ if TYPE_CHECKING: from beets.importer import ImportSession, ImportTask from beets.library import Album, Item, Library from beets.ui import Subcommand - from beets.util.id_extractors import RegexDict # TYPE_CHECKING guard is needed for any derived type # which uses an import from `beets.library` and `beets.imported` @@ -778,11 +778,6 @@ class MetadataSourcePlugin(Generic[R], BeetsPlugin, metaclass=abc.ABCMeta): super().__init__() self.config.add({"source_weight": 0.5}) - @property - @abc.abstractmethod - def id_regex(self) -> RegexDict: - raise NotImplementedError - @property @abc.abstractmethod def data_source(self) -> str: @@ -872,24 +867,9 @@ class MetadataSourcePlugin(Generic[R], BeetsPlugin, metaclass=abc.ABCMeta): return artist_string, artist_id - @staticmethod - def _get_id(url_type: str, id_: str, id_regex: RegexDict) -> str | None: - """Parse an ID from its URL if necessary. - - :param url_type: Type of URL. Either 'album' or 'track'. - :param id_: Album/track ID or URL. - :param id_regex: A dictionary containing a regular expression - extracting an ID from an URL (if it's not an ID already) in - 'pattern' and the number of the match group in 'match_group'. - :return: Album/track ID. - """ - log.debug("Extracting {} ID from '{}'", url_type, id_) - match = re.search(id_regex["pattern"].format(url_type), str(id_)) - if match: - id_ = match.group(id_regex["match_group"]) - if id_: - return id_ - return None + def _get_id(self, id_string: str) -> str | None: + """Parse release ID from the given ID string.""" + return extract_release_id(self.data_source.lower(), id_string) def candidates( self, diff --git a/beets/util/id_extractors.py b/beets/util/id_extractors.py index 4dbab087d..bbe2c32a4 100644 --- a/beets/util/id_extractors.py +++ b/beets/util/id_extractors.py @@ -14,47 +14,15 @@ """Helpers around the extraction of album/track ID's from metadata sources.""" +from __future__ import annotations + import re -from typing import TypedDict - -class RegexDict(TypedDict): - """A dictionary containing a regex pattern and the number of the - match group. - """ - - pattern: str - match_group: int - - -# Spotify IDs consist of 22 alphanumeric characters -# (zero-left-padded base62 representation of randomly generated UUID4) -spotify_id_regex: RegexDict = { - "pattern": r"(^|open\.spotify\.com/{}/)([0-9A-Za-z]{{22}})", - "match_group": 2, -} - -deezer_id_regex: RegexDict = { - "pattern": r"(^|deezer\.com/)([a-z]*/)?({}/)?(\d+)", - "match_group": 4, -} - -beatport_id_regex: RegexDict = { - "pattern": r"(^|beatport\.com/release/.+/)(\d+)$", - "match_group": 2, -} - -# A note on Bandcamp: There is no such thing as a Bandcamp album or artist ID, -# the URL can be used as the identifier. The Bandcamp metadata source plugin -# works that way - https://github.com/snejus/beetcamp. Bandcamp album -# URLs usually look like: https://nameofartist.bandcamp.com/album/nameofalbum - - -def extract_discogs_id_regex(album_id): - """Returns the Discogs_id or None.""" - # Discogs-IDs are simple integers. In order to avoid confusion with - # other metadata plugins, we only look for very specific formats of the - # input string: +PATTERN_BY_SOURCE = { + "spotify": re.compile(r"(?:^|open\.spotify\.com/[^/]+/)([0-9A-Za-z]{22})"), + "deezer": re.compile(r"(?:^|deezer\.com/)(?:[a-z]*/)?(?:[^/]+/)?(\d+)"), + "beatport": re.compile(r"(?:^|beatport\.com/release/.+/)(\d+)$"), + "musicbrainz": re.compile(r"(\w{8}(?:-\w{4}){3}-\w{12})"), # - plain integer, optionally wrapped in brackets and prefixed by an # 'r', as this is how discogs displays the release ID on its webpage. # - legacy url format: discogs.com/<name of release>/release/<id> @@ -62,15 +30,19 @@ def extract_discogs_id_regex(album_id): # - current url format: discogs.com/release/<id>-<name of release> # See #291, #4080 and #4085 for the discussions leading up to these # patterns. - # Regex has been tested here https://regex101.com/r/TOu7kw/1 + "discogs": re.compile( + r"(?:^|\[?r|discogs\.com/(?:[^/]+/)?release/)(\d+)\b" + ), + # There is no such thing as a Bandcamp album or artist ID, the URL can be + # used as the identifier. The Bandcamp metadata source plugin works that way + # - https://github.com/snejus/beetcamp. Bandcamp album URLs usually look + # like: https://nameofartist.bandcamp.com/album/nameofalbum + "bandcamp": re.compile(r"(.+)"), + "tidal": re.compile(r"([^/]+)$"), +} - for pattern in [ - r"^\[?r?(?P<id>\d+)\]?$", - r"discogs\.com/release/(?P<id>\d+)-?", - r"discogs\.com/[^/]+/release/(?P<id>\d+)", - ]: - match = re.search(pattern, album_id) - if match: - return int(match.group("id")) +def extract_release_id(source: str, id_: str) -> str | None: + if m := PATTERN_BY_SOURCE[source].search(str(id_)): + return m[1] return None diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index fab720c2b..d98fab722 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -30,7 +30,6 @@ import beets import beets.ui from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.plugins import BeetsPlugin, MetadataSourcePlugin, get_distance -from beets.util.id_extractors import beatport_id_regex AUTH_ERRORS = (TokenRequestDenied, TokenMissing, VerifierMissing) USER_AGENT = f"beets/{beets.__version__} +https://beets.io/" @@ -282,7 +281,6 @@ class BeatportTrack(BeatportObject): class BeatportPlugin(BeetsPlugin): data_source = "Beatport" - id_regex = beatport_id_regex def __init__(self): super().__init__() @@ -394,8 +392,7 @@ class BeatportPlugin(BeetsPlugin): """ self._log.debug("Searching for release {0}", release_id) - release_id = self._get_id("album", release_id, self.id_regex) - if release_id is None: + if not (release_id := self._get_id(release_id)): self._log.debug("Not a valid Beatport release ID.") return None diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index 25815e8d3..2e5d8473a 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -14,6 +14,8 @@ """Adds Deezer release and track search support to the autotagger""" +from __future__ import annotations + import collections import time @@ -25,7 +27,6 @@ from beets.autotag import AlbumInfo, TrackInfo from beets.dbcore import types from beets.library import DateType from beets.plugins import BeetsPlugin, MetadataSourcePlugin -from beets.util.id_extractors import deezer_id_regex class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): @@ -43,8 +44,6 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): album_url = "https://api.deezer.com/album/" track_url = "https://api.deezer.com/track/" - id_regex = deezer_id_regex - def __init__(self): super().__init__() @@ -75,21 +74,15 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): return None return data - def album_for_id(self, album_id): - """Fetch an album by its Deezer ID or URL and return an - AlbumInfo object or None if the album is not found. + def album_for_id(self, album_id: str) -> AlbumInfo | None: + """Fetch an album by its Deezer ID or URL.""" + if not (deezer_id := self._get_id(album_id)): + return None - :param album_id: Deezer ID or URL for the album. - :type album_id: str - :return: AlbumInfo object for album. - :rtype: beets.autotag.hooks.AlbumInfo or None - """ - deezer_id = self._get_id("album", album_id, self.id_regex) - if deezer_id is None: - return None - album_data = self.fetch_data(self.album_url + deezer_id) - if album_data is None: + album_url = f"{self.album_url}{deezer_id}" + if not (album_data := self.fetch_data(album_url)): return None + contributors = album_data.get("contributors") if contributors is not None: artist, artist_id = self.get_artist(contributors) @@ -132,7 +125,7 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): tracks_data.extend(tracks_obj["data"]) tracks = [] - medium_totals = collections.defaultdict(int) + medium_totals: dict[int | None, int] = collections.defaultdict(int) for i, track_data in enumerate(tracks_data, start=1): track = self._get_track(track_data) track.index = i @@ -150,13 +143,15 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): artist_id=artist_id, tracks=tracks, albumtype=album_data["record_type"], - va=len(album_data["contributors"]) == 1 - and artist.lower() == "various artists", + va=( + len(album_data["contributors"]) == 1 + and (artist or "").lower() == "various artists" + ), year=year, month=month, day=day, label=album_data["label"], - mediums=max(medium_totals.keys()), + mediums=max(filter(None, medium_totals.keys())), data_source=self.data_source, data_url=album_data["link"], cover_art_url=album_data.get("cover_xl"), @@ -204,12 +199,11 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): :rtype: beets.autotag.hooks.TrackInfo or None """ if track_data is None: - deezer_id = self._get_id("track", track_id, self.id_regex) - if deezer_id is None: - return None - track_data = self.fetch_data(self.track_url + deezer_id) - if track_data is None: + if not (deezer_id := self._get_id(track_id)) or not ( + track_data := self.fetch_data(f"{self.track_url}{deezer_id}") + ): return None + track = self._get_track(track_data) # Get album's tracks to set `track.index` (position on the entire diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 19521b035..a8d08c1e9 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -38,7 +38,7 @@ import beets.ui from beets import config from beets.autotag.hooks import AlbumInfo, TrackInfo, string_dist from beets.plugins import BeetsPlugin, MetadataSourcePlugin, get_distance -from beets.util.id_extractors import extract_discogs_id_regex +from beets.util.id_extractors import extract_release_id USER_AGENT = f"beets/{beets.__version__} +https://beets.io/" API_KEY = "rAzVUQYRaoFjeBjyWuWZ" @@ -266,7 +266,7 @@ class DiscogsPlugin(BeetsPlugin): """ self._log.debug("Searching for release {0}", album_id) - discogs_id = extract_discogs_id_regex(album_id) + discogs_id = extract_release_id("discogs", album_id) if not discogs_id: return None @@ -401,7 +401,7 @@ class DiscogsPlugin(BeetsPlugin): else: genre = base_genre - discogs_albumid = extract_discogs_id_regex(result.data.get("uri")) + discogs_albumid = extract_release_id("discogs", result.data.get("uri")) # Extract information for the optional AlbumInfo fields that are # contained on nested discogs fields. diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index e1a640d84..34a46715d 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -16,7 +16,6 @@ from __future__ import annotations -import re import traceback from collections import Counter from itertools import product @@ -28,13 +27,8 @@ import musicbrainzngs import beets import beets.autotag.hooks from beets import config, plugins, util -from beets.plugins import BeetsPlugin, MetadataSourcePlugin -from beets.util.id_extractors import ( - beatport_id_regex, - deezer_id_regex, - extract_discogs_id_regex, - spotify_id_regex, -) +from beets.plugins import BeetsPlugin +from beets.util.id_extractors import extract_release_id if TYPE_CHECKING: from collections.abc import Iterator, Sequence @@ -302,17 +296,6 @@ def _set_date_str( setattr(info, key, date_num) -def _parse_id(s: str) -> str | None: - """Search for a MusicBrainz ID in the given string and return it. If - no ID can be found, return None. - """ - # Find the first thing that looks like a UUID/MBID. - match = re.search("[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}", s) - if match is not None: - return match.group() if match else None - return None - - def _is_translation(r): _trans_key = "transl-tracklisting" return r["type"] == _trans_key and r["direction"] == "backward" @@ -753,24 +736,10 @@ class MusicBrainzPlugin(BeetsPlugin): source.capitalize(), ) - if "discogs" in urls: - info.discogs_albumid = extract_discogs_id_regex(urls["discogs"]) - if "bandcamp" in urls: - info.bandcamp_album_id = urls["bandcamp"] - if "spotify" in urls: - info.spotify_album_id = MetadataSourcePlugin._get_id( - "album", urls["spotify"], spotify_id_regex + for source, url in urls.items(): + setattr( + info, f"{source}_album_id", extract_release_id(source, url) ) - if "deezer" in urls: - info.deezer_album_id = MetadataSourcePlugin._get_id( - "album", urls["deezer"], deezer_id_regex - ) - if "beatport" in urls: - info.beatport_album_id = MetadataSourcePlugin._get_id( - "album", urls["beatport"], beatport_id_regex - ) - if "tidal" in urls: - info.tidal_album_id = urls["tidal"].split("/")[-1] extra_albumdatas = plugins.send("mb_album_extract", data=release) for extra_albumdata in extra_albumdatas: @@ -869,10 +838,10 @@ class MusicBrainzPlugin(BeetsPlugin): MusicBrainzAPIError. """ self._log.debug("Requesting MusicBrainz release {}", album_id) - albumid = _parse_id(album_id) - if not albumid: + if not (albumid := extract_release_id("musicbrainz", album_id)): self._log.debug("Invalid MBID ({0}).", album_id) return None + try: res = musicbrainzngs.get_release_by_id(albumid, RELEASE_INCLUDES) @@ -906,10 +875,10 @@ class MusicBrainzPlugin(BeetsPlugin): """Fetches a track by its MusicBrainz ID. Returns a TrackInfo object or None if no track is found. May raise a MusicBrainzAPIError. """ - trackid = _parse_id(track_id) - if not trackid: + if not (trackid := extract_release_id("musicbrainz", track_id)): self._log.debug("Invalid MBID ({0}).", track_id) return None + try: res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES) except musicbrainzngs.ResponseError: diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 44a0e0ce7..c0d212971 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -17,6 +17,8 @@ Spotify playlist construction. """ +from __future__ import annotations + import base64 import collections import json @@ -33,7 +35,6 @@ from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.dbcore import types from beets.library import DateType from beets.plugins import BeetsPlugin, MetadataSourcePlugin -from beets.util.id_extractors import spotify_id_regex DEFAULT_WAITING_TIME = 5 @@ -71,8 +72,6 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): track_url = "https://api.spotify.com/v1/tracks/" audio_features_url = "https://api.spotify.com/v1/audio-features/" - id_regex = spotify_id_regex - spotify_audio_features = { "acousticness": "spotify_acousticness", "danceability": "spotify_danceability", @@ -233,7 +232,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): self._log.error(f"Request failed. Error: {e}") raise SpotifyAPIError("Request failed.") - def album_for_id(self, album_id): + def album_for_id(self, album_id: str) -> AlbumInfo | None: """Fetch an album by its Spotify ID or URL and return an AlbumInfo object or None if the album is not found. @@ -242,8 +241,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): :return: AlbumInfo object for album :rtype: beets.autotag.hooks.AlbumInfo or None """ - spotify_id = self._get_id("album", album_id, self.id_regex) - if spotify_id is None: + if not (spotify_id := self._get_id(album_id)): return None album_data = self._handle_response( @@ -285,7 +283,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): tracks_items.extend(tracks_data["items"]) tracks = [] - medium_totals = collections.defaultdict(int) + medium_totals: dict[int | None, int] = collections.defaultdict(int) for i, track_data in enumerate(tracks_items, start=1): track = self._get_track(track_data) track.index = i @@ -309,7 +307,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): month=month, day=day, label=album_data["label"], - mediums=max(medium_totals.keys()), + mediums=max(filter(None, medium_totals.keys())), data_source=self.data_source, data_url=album_data["external_urls"]["spotify"], ) @@ -359,13 +357,14 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): :return: TrackInfo object for track :rtype: beets.autotag.hooks.TrackInfo or None """ - if track_data is None: - spotify_id = self._get_id("track", track_id, self.id_regex) - if spotify_id is None: + if not track_data: + if not (spotify_id := self._get_id(track_id)) or not ( + track_data := self._handle_response( + requests.get, f"{self.track_url}{spotify_id}" + ) + ): return None - track_data = self._handle_response( - requests.get, self.track_url + spotify_id - ) + track = self._get_track(track_data) # Get album's tracks to set `track.index` (position on the entire diff --git a/test/plugins/test_discogs.py b/test/plugins/test_discogs.py index 5e327ab27..eb9a625b1 100644 --- a/test/plugins/test_discogs.py +++ b/test/plugins/test_discogs.py @@ -21,7 +21,6 @@ import pytest from beets import config from beets.test._common import Bag from beets.test.helper import BeetsTestCase, capture_log -from beets.util.id_extractors import extract_discogs_id_regex from beetsplug.discogs import DiscogsPlugin @@ -369,37 +368,6 @@ class DGAlbumInfoTest(BeetsTestCase): assert d is None assert "Release does not contain the required fields" in logs[0] - def test_album_for_id(self): - """Test parsing for a valid Discogs release_id""" - test_patterns = [ - ( - "http://www.discogs.com/G%C3%BCnther-Lause-Meru-Ep/release/4354798", - 4354798, - ), - ( - "http://www.discogs.com/release/4354798-G%C3%BCnther-Lause-Meru-Ep", - 4354798, - ), - ( - "http://www.discogs.com/G%C3%BCnther-4354798Lause-Meru-Ep/release/4354798", # NOQA E501 - 4354798, - ), - ( - "http://www.discogs.com/release/4354798-G%C3%BCnther-4354798Lause-Meru-Ep/", # NOQA E501 - 4354798, - ), - ("[r4354798]", 4354798), - ("r4354798", 4354798), - ("4354798", 4354798), - ("yet-another-metadata-provider.org/foo/12345", ""), - ("005b84a0-ecd6-39f1-b2f6-6eb48756b268", ""), - ] - for test_pattern, expected in test_patterns: - match = extract_discogs_id_regex(test_pattern) - if not match: - match = "" - assert match == expected - def test_default_genre_style_settings(self): """Test genre default settings, genres to genre, styles to style""" release = self._make_release_from_positions(["1", "2"]) diff --git a/test/plugins/test_musicbrainz.py b/test/plugins/test_musicbrainz.py index b8640c870..0f142a353 100644 --- a/test/plugins/test_musicbrainz.py +++ b/test/plugins/test_musicbrainz.py @@ -662,24 +662,6 @@ class MBAlbumInfoTest(MusicBrainzTestCase): assert t[1].trackdisambig == "SECOND TRACK" -class ParseIDTest(BeetsTestCase): - def test_parse_id_correct(self): - id_string = "28e32c71-1450-463e-92bf-e0a46446fc11" - out = musicbrainz._parse_id(id_string) - assert out == id_string - - def test_parse_id_non_id_returns_none(self): - id_string = "blah blah" - out = musicbrainz._parse_id(id_string) - assert out is None - - def test_parse_id_url_finds_id(self): - id_string = "28e32c71-1450-463e-92bf-e0a46446fc11" - id_url = "https://musicbrainz.org/entity/%s" % id_string - out = musicbrainz._parse_id(id_url) - assert out == id_string - - class ArtistFlatteningTest(BeetsTestCase): def _credit_dict(self, suffix=""): return { diff --git a/test/test_plugins.py b/test/test_plugins.py index 417debbdd..3e809e492 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -30,16 +30,10 @@ from beets.importer import ( SingletonImportTask, ) from beets.library import Item -from beets.plugins import MetadataSourcePlugin from beets.test import helper from beets.test.helper import AutotagStub, ImportHelper, TerminalImportMixin from beets.test.helper import PluginTestCase as BasePluginTestCase from beets.util import displayable_path, syspath -from beets.util.id_extractors import ( - beatport_id_regex, - deezer_id_regex, - spotify_id_regex, -) class PluginLoaderTestCase(BasePluginTestCase): @@ -547,61 +541,3 @@ class PromptChoicesTest(TerminalImportMixin, PluginImportTestCase): self.mock_input_options.assert_called_once_with( opts, default="a", require=ANY ) - - -class ParseSpotifyIDTest(unittest.TestCase): - def test_parse_id_correct(self): - id_string = "39WqpoPgZxygo6YQjehLJJ" - out = MetadataSourcePlugin._get_id("album", id_string, spotify_id_regex) - assert out == id_string - - def test_parse_id_non_id_returns_none(self): - id_string = "blah blah" - out = MetadataSourcePlugin._get_id("album", id_string, spotify_id_regex) - assert out is None - - def test_parse_id_url_finds_id(self): - id_string = "39WqpoPgZxygo6YQjehLJJ" - id_url = "https://open.spotify.com/album/%s" % id_string - out = MetadataSourcePlugin._get_id("album", id_url, spotify_id_regex) - assert out == id_string - - -class ParseDeezerIDTest(unittest.TestCase): - def test_parse_id_correct(self): - id_string = "176356382" - out = MetadataSourcePlugin._get_id("album", id_string, deezer_id_regex) - assert out == id_string - - def test_parse_id_non_id_returns_none(self): - id_string = "blah blah" - out = MetadataSourcePlugin._get_id("album", id_string, deezer_id_regex) - assert out is None - - def test_parse_id_url_finds_id(self): - id_string = "176356382" - id_url = "https://www.deezer.com/album/%s" % id_string - out = MetadataSourcePlugin._get_id("album", id_url, deezer_id_regex) - assert out == id_string - - -class ParseBeatportIDTest(unittest.TestCase): - def test_parse_id_correct(self): - id_string = "3089651" - out = MetadataSourcePlugin._get_id( - "album", id_string, beatport_id_regex - ) - assert out == id_string - - def test_parse_id_non_id_returns_none(self): - id_string = "blah blah" - out = MetadataSourcePlugin._get_id( - "album", id_string, beatport_id_regex - ) - assert out is None - - def test_parse_id_url_finds_id(self): - id_string = "3089651" - id_url = "https://www.beatport.com/release/album-name/%s" % id_string - out = MetadataSourcePlugin._get_id("album", id_url, beatport_id_regex) - assert out == id_string diff --git a/test/util/test_id_extractors.py b/test/util/test_id_extractors.py new file mode 100644 index 000000000..8d4823419 --- /dev/null +++ b/test/util/test_id_extractors.py @@ -0,0 +1,34 @@ +import pytest + +from beets.util.id_extractors import extract_release_id + + +@pytest.mark.parametrize( + "source, id_string, expected", + [ + ("spotify", "39WqpoPgZxygo6YQjehLJJ", "39WqpoPgZxygo6YQjehLJJ"), + ("spotify", "blah blah", None), + ("spotify", "https://open.spotify.com/album/39WqpoPgZxygo6YQjehLJJ", "39WqpoPgZxygo6YQjehLJJ"), # noqa: E501 + ("deezer", "176356382", "176356382"), + ("deezer", "blah blah", None), + ("deezer", "https://www.deezer.com/album/176356382", "176356382"), + ("beatport", "3089651", "3089651"), + ("beatport", "blah blah", None), + ("beatport", "https://www.beatport.com/release/album-name/3089651", "3089651"), # noqa: E501 + ("discogs", "http://www.discogs.com/G%C3%BCnther-Lause-Meru-Ep/release/4354798", "4354798"), # noqa: E501 + ("discogs", "http://www.discogs.com/release/4354798-G%C3%BCnther-Lause-Meru-Ep", "4354798"), # noqa: E501 + ("discogs", "http://www.discogs.com/G%C3%BCnther-4354798Lause-Meru-Ep/release/4354798", "4354798"), # noqa: E501 + ("discogs", "http://www.discogs.com/release/4354798-G%C3%BCnther-4354798Lause-Meru-Ep/", "4354798"), # noqa: E501 + ("discogs", "[r4354798]", "4354798"), + ("discogs", "r4354798", "4354798"), + ("discogs", "4354798", "4354798"), + ("discogs", "yet-another-metadata-provider.org/foo/12345", None), + ("discogs", "005b84a0-ecd6-39f1-b2f6-6eb48756b268", None), + ("musicbrainz", "28e32c71-1450-463e-92bf-e0a46446fc11", "28e32c71-1450-463e-92bf-e0a46446fc11"), # noqa: E501 + ("musicbrainz", "blah blah", None), + ("musicbrainz", "https://musicbrainz.org/entity/28e32c71-1450-463e-92bf-e0a46446fc11", "28e32c71-1450-463e-92bf-e0a46446fc11"), # noqa: E501 + ("bandcamp", "https://nameofartist.bandcamp.com/album/nameofalbum", "https://nameofartist.bandcamp.com/album/nameofalbum"), # noqa: E501 + ], +) # fmt: skip +def test_extract_release_id(source, id_string, expected): + assert extract_release_id(source, id_string) == expected From 8936ae4e6fc865ed77274ff3e387fa633441f58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= <snejus@protonmail.com> Date: Sat, 17 May 2025 14:49:51 +0100 Subject: [PATCH 070/728] Test URL extraction against other sources --- test/util/test_id_extractors.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/util/test_id_extractors.py b/test/util/test_id_extractors.py index 8d4823419..4918b4361 100644 --- a/test/util/test_id_extractors.py +++ b/test/util/test_id_extractors.py @@ -1,3 +1,5 @@ +from typing import NamedTuple + import pytest from beets.util.id_extractors import extract_release_id @@ -32,3 +34,28 @@ from beets.util.id_extractors import extract_release_id ) # fmt: skip def test_extract_release_id(source, id_string, expected): assert extract_release_id(source, id_string) == expected + + +class SourceWithURL(NamedTuple): + source: str + url: str + + +source_with_urls = [ + SourceWithURL("spotify", "https://open.spotify.com/album/39WqpoPgZxygo6YQjehLJJ"), + SourceWithURL("deezer", "https://www.deezer.com/album/176356382"), + SourceWithURL("beatport", "https://www.beatport.com/release/album-name/3089651"), + SourceWithURL("discogs", "http://www.discogs.com/G%C3%BCnther-Lause-Meru-Ep/release/4354798"), + SourceWithURL("musicbrainz", "https://musicbrainz.org/entity/28e32c71-1450-463e-92bf-e0a46446fc11"), +] # fmt: skip + + +@pytest.mark.parametrize("source", [s.source for s in source_with_urls]) +@pytest.mark.parametrize("source_with_url", source_with_urls) +def test_match_source_url(source, source_with_url): + if source == source_with_url.source: + assert extract_release_id(source, source_with_url.url) + else: + assert not extract_release_id(source, source_with_url.url), ( + f"Source {source} pattern should not match {source_with_url.source} URL" + ) From b0238d934e008cd6e97dfb5ed1193527127df749 Mon Sep 17 00:00:00 2001 From: Martin Atukunda <matlads@gmail.com> Date: Wed, 9 Apr 2025 09:36:36 +0300 Subject: [PATCH 071/728] feat: plugins/web: use media session api for notifications. The Media Session API provides a way to customize media notifications. This commit updates the metadata for the media session whenever a new track (item) starts playing. https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API --- beetsplug/web/static/beets.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/beetsplug/web/static/beets.js b/beetsplug/web/static/beets.js index 97af70110..4167330ad 100644 --- a/beetsplug/web/static/beets.js +++ b/beetsplug/web/static/beets.js @@ -266,7 +266,9 @@ var AppView = Backbone.View.extend({ playItem: function(item) { var url = 'item/' + item.get('id') + '/file'; $('#player audio').attr('src', url); - $('#player audio').get(0).play(); + $('#player audio').get(0).play().then(() => { + this.updateMediaSession(item); + }); if (this.playingItem != null) { this.playingItem.entryView.setPlaying(false); @@ -275,6 +277,26 @@ var AppView = Backbone.View.extend({ this.playingItem = item; }, + updateMediaSession: function (item) { + if ("mediaSession" in navigator) { + album_id = item.get("album_id"); + album_art_url = "album/" + album_id + "/art"; + navigator.mediaSession.metadata = new MediaMetadata({ + title: item.get("title"), + artist: item.get("artist"), + album: item.get("album"), + artwork: [ + { src: album_art_url, sizes: "96x96" }, + { src: album_art_url, sizes: "128x128" }, + { src: album_art_url, sizes: "192x192" }, + { src: album_art_url, sizes: "256x256" }, + { src: album_art_url, sizes: "384x384" }, + { src: album_art_url, sizes: "512x512" }, + ], + }); + } + }, + audioPause: function() { this.playingItem.entryView.setPlaying(false); }, From 17147058755b518bd6506a5e018ae8af1bbf40bf Mon Sep 17 00:00:00 2001 From: Martin Atukunda <matlads@gmail.com> Date: Wed, 9 Apr 2025 09:48:46 +0300 Subject: [PATCH 072/728] doc: plugin/web: now shows notifications using Media Session API --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0f84f2473..31da975e2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,9 @@ Unreleased New features: +* :doc:`plugins/web`: Show notifications when a track plays. This uses the + Media Session API to customize media notifications. + Bug fixes: For packagers: From d1d58569e1929e8e2b194bb21044660ab7a77f73 Mon Sep 17 00:00:00 2001 From: Martin Atukunda <matlads@users.noreply.github.com> Date: Wed, 16 Apr 2025 20:50:37 +0300 Subject: [PATCH 073/728] Update beetsplug/web/static/beets.js don't pollute global scope with album_id Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- beetsplug/web/static/beets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/web/static/beets.js b/beetsplug/web/static/beets.js index 4167330ad..46bf71bd3 100644 --- a/beetsplug/web/static/beets.js +++ b/beetsplug/web/static/beets.js @@ -279,7 +279,7 @@ var AppView = Backbone.View.extend({ updateMediaSession: function (item) { if ("mediaSession" in navigator) { - album_id = item.get("album_id"); + const album_id = item.get("album_id"); album_art_url = "album/" + album_id + "/art"; navigator.mediaSession.metadata = new MediaMetadata({ title: item.get("title"), From a75d2b4aa63be0c97a931bfba1b518b2ba9ebe2e Mon Sep 17 00:00:00 2001 From: Martin Atukunda <matlads@users.noreply.github.com> Date: Wed, 16 Apr 2025 20:50:48 +0300 Subject: [PATCH 074/728] Update beetsplug/web/static/beets.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- beetsplug/web/static/beets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/web/static/beets.js b/beetsplug/web/static/beets.js index 46bf71bd3..eace4d27d 100644 --- a/beetsplug/web/static/beets.js +++ b/beetsplug/web/static/beets.js @@ -280,7 +280,7 @@ var AppView = Backbone.View.extend({ updateMediaSession: function (item) { if ("mediaSession" in navigator) { const album_id = item.get("album_id"); - album_art_url = "album/" + album_id + "/art"; + const album_art_url = "album/" + album_id + "/art"; navigator.mediaSession.metadata = new MediaMetadata({ title: item.get("title"), artist: item.get("artist"), From 992d376d1ba7c0e1db0a9932a1c4e6eeeac5f5c2 Mon Sep 17 00:00:00 2001 From: Martin Atukunda <matlads@gmail.com> Date: Fri, 25 Apr 2025 12:53:07 +0300 Subject: [PATCH 075/728] feat(plugin/web): add artist and album to the item entry template --- beetsplug/web/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/web/templates/index.html b/beetsplug/web/templates/index.html index 0fdd46d15..693c8e815 100644 --- a/beetsplug/web/templates/index.html +++ b/beetsplug/web/templates/index.html @@ -45,7 +45,7 @@ <!-- Templates. --> <script type="text/template" id="item-entry-template"> - <%= title %> + <%= artist %> – <%= album %> – <%= title %> <span class="playing">▶</span> </script> <script type="text/template" id="item-main-detail-template"> From 7c799beda8f27e515ad6df9b48d24c1633ac6f84 Mon Sep 17 00:00:00 2001 From: Martin Atukunda <matlads@gmail.com> Date: Fri, 25 Apr 2025 12:54:15 +0300 Subject: [PATCH 076/728] style(plugin/web): run djlint over html to clean it up a bit --- beetsplug/web/templates/index.html | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/beetsplug/web/templates/index.html b/beetsplug/web/templates/index.html index 693c8e815..a290a952e 100644 --- a/beetsplug/web/templates/index.html +++ b/beetsplug/web/templates/index.html @@ -1,14 +1,17 @@ <!DOCTYPE html> -<html> +<html lang="en"> <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content="the music geek’s media organizer"> + <meta name="keywords" + content="beets, media, music, library, metadata, player, tagger, grep, transcoder, organizer"> <title>beets - - + href="{{ url_for('static', filename='beets.css') }}" + type="text/css"> - + @@ -17,18 +20,14 @@

beets

- - - - +
-
@@ -36,13 +35,8 @@
- -
-
- -
-
- +
+