├── test ├── __init__.py ├── fixtures │ ├── ok.flac │ ├── ok.mp3 │ ├── ok.ogg │ ├── crc.flac │ ├── fail.mp3 │ ├── md5.flac │ ├── truncated.flac │ ├── truncated.mp3 │ └── truncated.ogg ├── helper.py ├── integration_test.py └── cli_test.py ├── MANIFEST.in ├── .gitignore ├── .coveragerc ├── DEVELOPMENT.md ├── LICENSE ├── CHANGELOG.md ├── pyproject.toml ├── .github └── workflows │ └── main.yaml ├── README.md ├── beetsplug └── check.py └── uv.lock /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune test 2 | include LICENSE README.md 3 | -------------------------------------------------------------------------------- /test/fixtures/ok.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geigerzaehler/beets-check/HEAD/test/fixtures/ok.flac -------------------------------------------------------------------------------- /test/fixtures/ok.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geigerzaehler/beets-check/HEAD/test/fixtures/ok.mp3 -------------------------------------------------------------------------------- /test/fixtures/ok.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geigerzaehler/beets-check/HEAD/test/fixtures/ok.ogg -------------------------------------------------------------------------------- /test/fixtures/crc.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geigerzaehler/beets-check/HEAD/test/fixtures/crc.flac -------------------------------------------------------------------------------- /test/fixtures/fail.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geigerzaehler/beets-check/HEAD/test/fixtures/fail.mp3 -------------------------------------------------------------------------------- /test/fixtures/md5.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geigerzaehler/beets-check/HEAD/test/fixtures/md5.flac -------------------------------------------------------------------------------- /test/fixtures/truncated.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geigerzaehler/beets-check/HEAD/test/fixtures/truncated.flac -------------------------------------------------------------------------------- /test/fixtures/truncated.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geigerzaehler/beets-check/HEAD/test/fixtures/truncated.mp3 -------------------------------------------------------------------------------- /test/fixtures/truncated.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geigerzaehler/beets-check/HEAD/test/fixtures/truncated.ogg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | beets_check.egg-info/ 3 | build/ 4 | .coverage 5 | coverage/ 6 | *.pyc 7 | .tox/ 8 | htmlcov 9 | .noseids 10 | /venv 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */pyshared/* 4 | */python?.?/* 5 | */site-packages/nose/* 6 | */test/* 7 | beetsplug/__init__.py 8 | exclude_lines = 9 | raise NotImplementedError 10 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development guide 2 | 3 | ## Releases 4 | 5 | To release a new version of this project follow these steps: 6 | 7 | 1. `git fetch && git checkout -B release/candidate origin/main` 8 | 1. Replace the “Upcoming” heading of the changelog with the new version number 9 | and date of release. 10 | 1. Update the version in `pyproject.toml` 11 | 1. Run `uv lock` 12 | 1. Commit the changes with the commit message “release: vX.Y.Z” 13 | 1. Push the changes `git push origin` and wait for the build to pass. 14 | 1. Build the package: `rm -rf dist && uv build` 15 | 1. Publish the package: `uv publish` 16 | 1. Merge release candidate into main branch 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Thomas Scholtes. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.15.3 2025-11-04 4 | 5 | - Flac files can now be fixed properly with `beet check --fix` 6 | 7 | ## v0.15.2 2025-10-30 8 | 9 | - Files are automatically fixed during import if `auto-fix: true`. 10 | 11 | ## v0.15.1 2025-10-25 12 | 13 | - Add support for beets 2.5 14 | - Drop support for Python 3.9 and beets<2 15 | 16 | ## v0.15.0 2024-09-20 17 | 18 | - Don’t run custom external programs with `-v` (e.g. `ffmpeg -v`) to determine 19 | whether they are available. (Fixes #43) 20 | - Require Python >=3.9 21 | 22 | ## v0.14.1 2024-07-11 23 | 24 | - Require beets >=1.6.1 and support beets v2.x 25 | 26 | ## v0.14.0 2024-02-12 27 | 28 | - Require Python ^3.8 29 | - Require beets ^1.6 30 | 31 | ## v0.13.0 2020-06-27 32 | 33 | - Drop support for Python2.7 34 | - Require `beets>=1.4.7` 35 | - Fix a crash in `beet check --add` when a music file is not found on disk. (@ssssam) 36 | 37 | ## v0.12.1 2020-04-19 38 | 39 | - Fix crash when running `beet import` with threading enabled ([#22](https://github.com/geigerzaehler/beets-check/issues/22)) ([@alebianco](https://github.com/alebianco)) 40 | 41 | ## v0.12.0 2019-08-12 42 | 43 | - Add support for Python 3 44 | - Drop support for `beets<=1.3.10` 45 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "beets-check" 3 | version = "0.15.3" 4 | description = "beets plugin verifying file integrity with checksums" 5 | authors = [{ name = "Thomas Scholtes", email = "geigerzaehler@axiom.fm" }] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "http://www.github.com/geigerzaehler/beets-check" 9 | classifiers = [ 10 | "Topic :: Multimedia :: Sound/Audio", 11 | "Topic :: Multimedia :: Sound/Audio :: Players :: MP3", 12 | "License :: OSI Approved :: MIT License", 13 | "Environment :: Console", 14 | "Environment :: Web Environment", 15 | "Programming Language :: Python :: 3", 16 | ] 17 | packages = [{ include = "beetsplug" }] 18 | dependencies = ["beets >=2, <3", "mediafile >=0.13.0"] 19 | requires-python = ">=3.10" 20 | 21 | [dependency-groups] 22 | dev = ["pytest >=8", "ruff >=0.14"] 23 | 24 | [tool.ruff] 25 | target-version = "py39" 26 | unsafe-fixes = true 27 | preview = true 28 | 29 | [tool.ruff.lint] 30 | extend-select = [ 31 | "I", # Sort imports 32 | "C", # Pyflakes conventions 33 | # "PTH", # Use pathlib instead of os 34 | "PIE", # Misc. lints 35 | "UP", # Enforce modern Python syntax 36 | "FURB", # Also enforce more modern Python syntax 37 | "PT", # Pytest style 38 | "B", # Bugbear, avoid common sources of bugs 39 | "SIM", # Simplify 40 | "T20", # Warn about `print()` 41 | "RUF", 42 | "C4", # List comprehension 43 | ] 44 | ignore = [ 45 | # Pyright checks for unused imports and does it better. 46 | "F401", 47 | # ternary can be less readable 48 | "SIM108", 49 | # Ignore complexity 50 | "C901", 51 | ] 52 | 53 | [build-system] 54 | requires = ["hatchling"] 55 | build-backend = "hatchling.build" 56 | 57 | [tool.hatch.build.targets.sdist] 58 | include = ["beetsplug/*.py"] 59 | 60 | [tool.hatch.build.targets.wheel] 61 | include = ["beetsplug/*.py"] 62 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Check and test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | FORCE_COLOR: "1" 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | python-version: 17 | - "3.10" # minimum required 18 | - "3.13" 19 | - "3.14" 20 | 21 | runs-on: ubuntu-latest 22 | continue-on-error: ${{ matrix.python-version == '3.14' }} 23 | 24 | steps: 25 | - run: sudo apt-get install flac mp3val oggz-tools 26 | - uses: actions/checkout@v4 27 | - uses: astral-sh/setup-uv@v2 28 | with: 29 | enable-cache: true 30 | - uses: actions/setup-python@v5 31 | id: setup-python 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | - run: echo "UV_PYTHON=${{ steps.setup-python.outputs.python-path }}" >> "$GITHUB_ENV" 35 | - run: uv lock --locked 36 | - run: uv sync --all-extras --dev 37 | - run: uv run ruff check . 38 | - run: uv run pytest 39 | 40 | build-beets-versions: 41 | strategy: 42 | matrix: 43 | beets: 44 | - "beets@git+https://github.com/beetbox/beets#master" 45 | - "beets==2.0.*" 46 | - "beets==2.1.*" 47 | - "beets==2.2.*" 48 | - "beets==2.3.*" 49 | - "beets==2.4.*" 50 | 51 | runs-on: ubuntu-latest 52 | continue-on-error: ${{ endsWith(matrix.beets, 'master') }} 53 | 54 | steps: 55 | - run: sudo apt-get install flac mp3val oggz-tools 56 | - uses: actions/checkout@v4 57 | - uses: astral-sh/setup-uv@v2 58 | with: 59 | enable-cache: true 60 | - uses: actions/setup-python@v5 61 | id: setup-python 62 | with: 63 | python-version: 3.13 64 | - run: echo "UV_PYTHON=${{ steps.setup-python.outputs.python-path }}" >> "$GITHUB_ENV" 65 | - run: uv lock -P ${{ matrix.beets }} 66 | - run: uv sync --all-extras --dev 67 | - run: uv run pytest 68 | -------------------------------------------------------------------------------- /test/helper.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import shutil 4 | import sys 5 | import tempfile 6 | from collections import defaultdict 7 | from contextlib import contextmanager 8 | from io import StringIO 9 | 10 | import beets 11 | from beets import autotag, plugins 12 | from beets.autotag import ( 13 | AlbumInfo, 14 | AlbumMatch, 15 | Proposal, 16 | Recommendation, 17 | TrackInfo, 18 | TrackMatch, 19 | ) 20 | 21 | try: 22 | from beets.autotag.distance import Distance 23 | except ImportError: 24 | # beets<2.4 compatibility 25 | from beets.autotag.hooks import Distance 26 | 27 | import beets.plugins 28 | from beets.library import Item 29 | from mediafile import MediaFile 30 | 31 | from beetsplug import check, convert 32 | 33 | logging.getLogger("beets").propagate = True 34 | 35 | _beets_version = tuple(map(int, beets.__version__.split(".")[0:3])) 36 | 37 | 38 | class LogCapture(logging.Handler): 39 | def __init__(self): 40 | super().__init__() 41 | self.messages = [] 42 | 43 | def emit(self, record): 44 | self.messages.append(str(record.msg)) 45 | 46 | 47 | @contextmanager 48 | def captureLog(logger="beets"): 49 | capture = LogCapture() 50 | log = logging.getLogger(logger) 51 | log.addHandler(capture) 52 | try: 53 | yield capture.messages 54 | finally: 55 | log.removeHandler(capture) 56 | 57 | 58 | @contextmanager 59 | def captureStdout(): 60 | org = sys.stdout 61 | sys.stdout = StringIO() 62 | try: 63 | yield sys.stdout 64 | finally: 65 | sys.stdout = org 66 | 67 | 68 | @contextmanager 69 | def controlStdin(input=None): 70 | org = sys.stdin 71 | sys.stdin = StringIO(input) 72 | try: 73 | yield sys.stdin 74 | finally: 75 | sys.stdin = org 76 | 77 | 78 | class TestHelper: 79 | def setUp(self): 80 | self.temp_dir = tempfile.mkdtemp() 81 | self.disableIntegrityCheckers() 82 | 83 | def tearDown(self): 84 | self.unloadPlugins() 85 | if hasattr(self, "temp_dir"): 86 | shutil.rmtree(self.temp_dir) 87 | MockChecker.restore() 88 | 89 | def setupBeets(self): 90 | os.environ["BEETSDIR"] = self.temp_dir 91 | 92 | self.config = beets.config 93 | self.config.clear() 94 | self.config.read() 95 | 96 | self.config["plugins"] = [] 97 | self.config["verbose"] = True 98 | self.config["ui"]["color"] = False 99 | self.config["threaded"] = False 100 | self.config["import"]["copy"] = False 101 | 102 | self.libdir = os.path.join(self.temp_dir, "libdir") 103 | os.mkdir(self.libdir) 104 | self.config["directory"] = self.libdir 105 | 106 | self.lib = beets.library.Library( 107 | self.config["library"].as_filename(), self.libdir 108 | ) 109 | 110 | if _beets_version > (2, 3, 1): 111 | beets.plugins._instances = [ 112 | check.CheckPlugin(), 113 | convert.ConvertPlugin(), 114 | ] 115 | else: 116 | beets.plugins._classes = {check.CheckPlugin, convert.ConvertPlugin} 117 | beets.plugins._instances = {} 118 | 119 | self.fixture_dir = os.path.join(os.path.dirname(__file__), "fixtures") 120 | 121 | def setupImportDir(self, files): 122 | self.import_dir = os.path.join(self.temp_dir, "import") 123 | if not os.path.isdir(self.import_dir): 124 | os.mkdir(self.import_dir) 125 | for file in files: 126 | src = os.path.join(self.fixture_dir, file) 127 | shutil.copy(src, self.import_dir) 128 | 129 | def setupFixtureLibrary(self): 130 | for basename in os.listdir(self.fixture_dir): 131 | item = self.addItemFixture(basename) 132 | check.set_checksum(item) 133 | 134 | def addIntegrityFailFixture(self, checksum=True): 135 | """Add item with integrity errors to the library and return it. 136 | 137 | The `MockChecker` will raise an integrity error when run on this item. 138 | """ 139 | item = self.addItemFixture("truncated.mp3") 140 | if checksum: 141 | check.set_checksum(item) 142 | return item 143 | 144 | def addCorruptedFixture(self): 145 | """Add item with a wrong checksum to the library and return it.""" 146 | item = self.addItemFixture("ok.ogg") 147 | item["checksum"] = "this is a wrong checksum" 148 | item.store() 149 | return item 150 | 151 | def addItemFixture(self, basename): 152 | src = os.path.join(self.fixture_dir, basename) 153 | dst = os.path.join(self.libdir, basename) 154 | shutil.copy(src, dst) 155 | item = Item.from_path(dst) 156 | item.add(self.lib) 157 | return item 158 | 159 | def disableIntegrityCheckers(self): 160 | check.IntegrityChecker._all = [] 161 | check.IntegrityChecker._all_available = [] 162 | 163 | def enableIntegrityCheckers(self): 164 | if hasattr(check.IntegrityChecker, "_all"): 165 | delattr(check.IntegrityChecker, "_all") 166 | if hasattr(check.IntegrityChecker, "_all_available"): 167 | delattr(check.IntegrityChecker, "_all_available") 168 | 169 | def modifyFile(self, path, title="a different title"): 170 | mediafile = MediaFile(path) 171 | mediafile.title = title 172 | mediafile.save() 173 | 174 | @contextmanager 175 | def mockAutotag(self): 176 | mock = AutotagMock() 177 | mock.install() 178 | try: 179 | yield 180 | finally: 181 | mock.restore() 182 | 183 | def unloadPlugins(self): 184 | if _beets_version > (2, 3, 1): 185 | beets.plugins.BeetsPlugin.listeners = defaultdict(list) 186 | else: 187 | for plugin in beets.plugins._classes: # type: ignore (compatibility with beets<2.4) 188 | # Instantiating a plugin will modify register event listeners which 189 | # are stored in a class variable 190 | plugin.listeners = None # type: ignore (compatibility with beets<2.4) 191 | 192 | 193 | class AutotagMock: 194 | def __init__(self): 195 | self.id = 0 196 | 197 | def nextid(self): 198 | self.id += 1 199 | return self.id 200 | 201 | def install(self): 202 | self._orig_tag_album = autotag.tag_album 203 | self._orig_tag_item = autotag.tag_item 204 | autotag.tag_album = self.tag_album 205 | autotag.tag_item = self.tag_item 206 | 207 | def restore(self): 208 | autotag.tag_album = self._orig_tag_album 209 | autotag.tag_item = self._orig_tag_item 210 | 211 | def tag_album(self, items, **kwargs): 212 | artist = (items[0].artist or "") + " tag" 213 | album = (items[0].album or "") + " tag" 214 | mapping = {} 215 | dist = Distance() 216 | dist.tracks = {} 217 | for item in items: 218 | title = (item.title or "") + " tag" 219 | track_info = TrackInfo(title=title, track_id=self.nextid(), index=1) 220 | mapping[item] = track_info 221 | dist.tracks[track_info] = Distance() 222 | 223 | album_info = AlbumInfo( 224 | album="album", 225 | album_id=self.nextid(), 226 | artist="artist", 227 | artist_id=self.nextid(), 228 | tracks=list(mapping.values()), 229 | ) 230 | match = AlbumMatch( 231 | distance=dist, 232 | info=album_info, 233 | mapping=mapping, 234 | extra_items=[], 235 | extra_tracks=[], 236 | ) 237 | return artist, album, Proposal([match], Recommendation.strong) 238 | 239 | def tag_item(self, item, **kwargs): 240 | title = (item.title or "") + " tag" 241 | track_info = TrackInfo(title=title, track_id=self.nextid()) 242 | match = TrackMatch(distance=Distance(), info=track_info) 243 | return Proposal([match], Recommendation.strong) 244 | 245 | 246 | class MockChecker: 247 | name = "mock" 248 | 249 | @classmethod 250 | def install(cls): 251 | check.IntegrityChecker._all_available = [cls()] 252 | 253 | @classmethod 254 | def restore(cls): 255 | if hasattr(check.IntegrityChecker, "_all_available"): 256 | delattr(check.IntegrityChecker, "_all_available") 257 | 258 | @classmethod 259 | def installNone(cls): 260 | check.IntegrityChecker._all_available = [] 261 | 262 | def check(self, item): 263 | if b"truncated" in item.path or b'fail' in item.path: 264 | raise check.IntegrityError(item.path, "file is corrupt") 265 | 266 | def can_fix(self, item): 267 | return True 268 | 269 | def fix(self, item): 270 | mf = MediaFile(item.path) 271 | 272 | if b'truncated' in item.path: 273 | mf.url = "fixed" 274 | mf.save() 275 | 276 | if b"fail" in item.path: 277 | raise check.IntegrityError(item.path, "cannot fix file") 278 | -------------------------------------------------------------------------------- /test/integration_test.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import re 3 | from unittest import TestCase 4 | 5 | import beets 6 | import beets.library 7 | import beets.plugins 8 | import beets.ui 9 | from mediafile import MediaFile 10 | 11 | from beetsplug.check import IntegrityChecker, verify_checksum 12 | from test.helper import MockChecker, TestHelper, captureLog, captureStdout, controlStdin 13 | 14 | 15 | class ImportTest(TestHelper, TestCase): 16 | def setUp(self): 17 | super().setUp() 18 | self.setupBeets() 19 | self.setupImportDir(["ok.mp3"]) 20 | IntegrityChecker._all_available = [] 21 | 22 | def tearDown(self): 23 | super().tearDown() 24 | MockChecker.restore() 25 | 26 | def test_add_album_checksum(self): 27 | with self.mockAutotag(): 28 | beets.ui._raw_main(["import", self.import_dir]) 29 | item = self.lib.items().get() 30 | assert "checksum" in item 31 | assert item.title == "ok tag" 32 | verify_checksum(item) 33 | 34 | def test_add_singleton_checksum(self): 35 | with self.mockAutotag(): 36 | beets.ui._raw_main(["import", "--singletons", self.import_dir]) 37 | item = self.lib.items().get() 38 | assert "checksum" in item 39 | verify_checksum(item) 40 | 41 | def test_add_album_checksum_without_autotag(self): 42 | with self.mockAutotag(): 43 | beets.ui._raw_main(["import", "--noautotag", self.import_dir]) 44 | item = self.lib.items().get() 45 | assert "checksum" in item 46 | assert item.title == "ok" 47 | verify_checksum(item) 48 | 49 | def test_add_singleton_checksum_without_autotag(self): 50 | with self.mockAutotag(): 51 | beets.ui._raw_main([ 52 | "import", 53 | "--singletons", 54 | "--noautotag", 55 | self.import_dir, 56 | ]) 57 | item = self.lib.items().get() 58 | assert "checksum" in item 59 | verify_checksum(item) 60 | 61 | def test_reimport_does_not_overwrite_checksum(self): 62 | self.setupFixtureLibrary() 63 | 64 | item = self.lib.items().get() 65 | orig_checksum = item["checksum"] 66 | verify_checksum(item) 67 | self.modifyFile(item.path, "changed") 68 | 69 | with self.mockAutotag(): 70 | beets.ui._raw_main(["import", self.libdir]) 71 | 72 | item = self.lib.items([item.path.decode("utf-8")]).get() 73 | assert item["checksum"] == orig_checksum 74 | 75 | def test_skip_corrupt_files(self): 76 | MockChecker.install() 77 | self.setupImportDir(["ok.mp3", "truncated.mp3"]) 78 | 79 | with ( 80 | self.mockAutotag(), 81 | controlStdin(" "), 82 | captureStdout() as stdout, 83 | captureLog() as logs, 84 | ): 85 | beets.ui._raw_main(["import", self.import_dir]) 86 | 87 | assert "check: Warning: failed to verify integrity" in logs 88 | assert "truncated.mp3: file is corrupt" in "\n".join(logs) 89 | assert "Do you want to skip this album" in stdout.getvalue() 90 | assert len(self.lib.items()) == 0 91 | 92 | def test_quiet_skip_corrupt_files(self): 93 | MockChecker.install() 94 | self.setupImportDir(["ok.mp3", "truncated.mp3"]) 95 | 96 | with self.mockAutotag(), captureLog() as logs: 97 | beets.ui._raw_main(["import", "-q", self.import_dir]) 98 | 99 | assert "check: Warning: failed to verify integrity" in logs 100 | assert "truncated.mp3: file is corrupt\ncheck: Skipping." in "\n".join(logs) 101 | assert len(self.lib.items()) == 0 102 | 103 | def test_add_corrupt_files(self): 104 | MockChecker.install() 105 | self.setupImportDir(["ok.mp3", "truncated.mp3"]) 106 | 107 | with self.mockAutotag(), controlStdin("n"): 108 | beets.ui._raw_main(["import", self.import_dir]) 109 | 110 | assert len(self.lib.items()) == 2 111 | item = self.lib.items("truncated").get() 112 | mediafile = MediaFile(item.path) 113 | assert mediafile.title == "truncated tag" 114 | 115 | def test_fix_corrupt_files(self): 116 | self.config["check"]["auto-fix"] = True 117 | 118 | MockChecker.install() 119 | self.setupImportDir(["ok.mp3", "truncated.mp3"]) 120 | 121 | with self.mockAutotag(), captureLog() as logs: 122 | beets.ui._raw_main(["import", self.import_dir]) 123 | 124 | assert len(self.lib.items()) == 2 125 | assert "Fixing file:" in "\n".join(logs) 126 | 127 | item = self.lib.items("truncated").get() 128 | verify_checksum(item) 129 | 130 | mediafile = MediaFile(item.path) 131 | assert mediafile.url == "fixed" 132 | 133 | def test_fix_corrupt_files_fail_skip(self): 134 | self.config["check"]["auto-fix"] = True 135 | 136 | MockChecker.install() 137 | self.setupImportDir(["ok.mp3", "fail.mp3"]) 138 | 139 | with self.mockAutotag(), captureLog() as logs, controlStdin("y"): 140 | beets.ui._raw_main(["import", self.import_dir]) 141 | 142 | assert len(self.lib.items()) == 0 143 | assert "Fixing file:" in "\n".join(logs) 144 | assert "Failed to fix" in "\n".join(logs) 145 | 146 | def test_fix_corrupt_files_fail(self): 147 | self.config["check"]["auto-fix"] = True 148 | 149 | MockChecker.install() 150 | self.setupImportDir(["ok.mp3", "fail.mp3"]) 151 | 152 | with self.mockAutotag(), captureLog() as logs, controlStdin("n"): 153 | beets.ui._raw_main(["import", self.import_dir]) 154 | 155 | assert len(self.lib.items()) == 2 156 | assert "Fixing file:" in "\n".join(logs) 157 | assert "Failed to fix" in "\n".join(logs) 158 | 159 | def test_fix_corrupt_files_quiet(self): 160 | self.config["check"]["auto-fix"] = True 161 | self.config["import"]["quiet"] = True 162 | 163 | MockChecker.install() 164 | self.setupImportDir(["ok.mp3", "fail.mp3"]) 165 | 166 | with self.mockAutotag(), captureLog() as logs: 167 | beets.ui._raw_main(["import", self.import_dir]) 168 | 169 | assert len(self.lib.items()) == 0 170 | assert "Fixing file:" in "\n".join(logs) 171 | assert "Failed to fix" in "\n".join(logs) 172 | 173 | 174 | class WriteTest(TestHelper, TestCase): 175 | def setUp(self): 176 | super().setUp() 177 | self.setupBeets() 178 | self.setupFixtureLibrary() 179 | 180 | def test_log_error_for_invalid_checksum(self): 181 | item = self.lib.items("ok").get() 182 | verify_checksum(item) 183 | self.modifyFile(item.path) 184 | 185 | with captureLog() as logs: 186 | beets.ui._raw_main(["write", item.title]) 187 | assert re.search( 188 | r"error reading .*: checksum did not match value in library", 189 | "\n".join(logs), 190 | ) 191 | 192 | def test_abort_write_when_invalid_checksum(self): 193 | item = self.lib.items("ok").get() 194 | verify_checksum(item) 195 | self.modifyFile(item.path, title="other title") 196 | 197 | item["title"] = "newtitle" 198 | item.store() 199 | beets.ui._raw_main(["write", item.title]) 200 | 201 | mediafile = MediaFile(item.path) 202 | assert mediafile.title != "newtitle" 203 | 204 | def test_write_on_integrity_error(self): 205 | MockChecker.install() 206 | 207 | item = self.lib.items("truncated").get() 208 | 209 | item["title"] = "newtitle" 210 | item.store() 211 | beets.ui._raw_main(["write", item.title]) 212 | 213 | item["checksum"] = "" 214 | item.load() 215 | verify_checksum(item) 216 | mediafile = MediaFile(item.path) 217 | assert mediafile.title == "newtitle" 218 | 219 | def test_update_checksum(self): 220 | item = self.lib.items("ok").get() 221 | orig_checksum = item["checksum"] 222 | verify_checksum(item) 223 | 224 | item["title"] = "newtitle" 225 | item.store() 226 | beets.ui._raw_main(["write", item.title]) 227 | 228 | item["checksum"] = "" 229 | item.load() 230 | assert item["checksum"] != orig_checksum 231 | verify_checksum(item) 232 | 233 | mediafile = MediaFile(item.path) 234 | assert mediafile.title == "newtitle" 235 | 236 | 237 | class ConvertTest(TestHelper, TestCase): 238 | def setUp(self): 239 | super().setUp() 240 | self.setupBeets() 241 | 242 | beets.config["convert"] = { 243 | "dest": os.path.join(self.temp_dir, "convert"), 244 | # Truncated copy to break checksum 245 | "command": "dd bs=1024 count=6 if=$source of=$dest", 246 | } 247 | self.setupFixtureLibrary() 248 | 249 | def test_convert_command(self): 250 | with controlStdin("y"): 251 | beets.ui._raw_main(["convert", "ok.ogg"]) 252 | 253 | def test_update_after_keep_new_convert(self): 254 | item = self.lib.items("ok.ogg").get() 255 | verify_checksum(item) 256 | 257 | with controlStdin("y"): 258 | beets.ui._raw_main(["convert", "--keep-new", "ok.ogg"]) 259 | 260 | converted = self.lib.items("ok.ogg").get() 261 | assert converted.path != item.path 262 | assert converted.checksum != item.checksum 263 | verify_checksum(converted) 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beets-check 2 | 3 | [![Build Status](https://travis-ci.org/geigerzaehler/beets-check.svg?branch=master)](https://travis-ci.org/geigerzaehler/beets-check) 4 | [![Coverage Status](https://coveralls.io/repos/geigerzaehler/beets-check/badge.png?branch=master)](https://coveralls.io/r/geigerzaehler/beets-check?branch=master) 5 | 6 | _The [beets][] plugin for paranoid obsessive-compulsive music geeks._ 7 | 8 | _beets-check_ lets you verify the integrity of your audio files. It computes 9 | and validates file checksums and uses third party tools to run custom 10 | tests on files. 11 | 12 | This plugin requires at least version 2 of beets and at least Python 3.10 13 | 14 | ``` 15 | pip install --upgrade beets>=2 16 | pip install git+git://github.com/geigerzaehler/beets-check.git@main 17 | ``` 18 | 19 | Then add `check` to the list of plugins in your beet configuration. 20 | (Running `beet config --edit` might be the quickest way.) 21 | 22 | If you want to use third-party tools to test your audio files you have 23 | to manually install them on your system. Run `beet check --list-tools` 24 | to see a list of programs the plugin can use or [add your 25 | own](#third-party-tests). 26 | 27 | ## Usage 28 | 29 | Let’s get started and add some checksums to your library. 30 | 31 | ``` 32 | $ beet check -a 33 | WARNING integrity error: /music/Abbey Road/01 Come Together.mp3 34 | Adding unknown checksums: 1032/8337 [12%] 35 | ``` 36 | 37 | The `check` command looks for all files that don’t have a checksum yet. 38 | It computes the checksum for each of these files and stores it in the 39 | database. The command also prints a warning if one of the third-party 40 | tools finds an error. (More on those [later](#third-party-tests).) 41 | 42 | After some time (or maybe a system crash) you’ll probably want to go back to 43 | your library and verify that none of the files have changed. To do this run 44 | 45 | ``` 46 | $ beet check 47 | FAILED: /music/Sgt. Pepper/13 A Day in the Life.mp3 48 | Verifying checksums: 5102/8337 [53%] 49 | ``` 50 | 51 | For later inspection you might want to keep a log. To do that just 52 | redirect the error output with `beet check 2>check.log`. All `WARNING` 53 | and `ERROR` lines are sent to stderr, so you will still see the 54 | progressbar. 55 | 56 | When you change your files through beets, using the `modfiy` command 57 | for example, the plugin will [update the checksums 58 | automatically](#automatic-update). However, if you change files 59 | manually, you also need to update the checksums manually. 60 | 61 | ``` 62 | $ beet check -u 'album:Sgt. Pepper' 63 | Updating checksums: 2/13 [15%] 64 | ``` 65 | 66 | ### Third-party Tests 67 | 68 | The plugin allows you to add custom file checks through external tools. 69 | The plugin supports `flac --test`, `oggz-validate`, and `mp3val` out of 70 | the box, but you can also [configure your own](#third-party-tools). 71 | 72 | Custom tests are run when on the following occasions. 73 | 74 | - Before importing a file (see below) 75 | - Before adding checksums with the `-a` flag 76 | - When running `beet check --external` 77 | 78 | The file checks are not run when updating files. The rationale is that 79 | if the checksum of a file is correct, the file is assumed to be clean 80 | and pass all the custom tests. 81 | 82 | If some file fails a test the line 83 | 84 | ``` 85 | WARNING error description: /path/to/file 86 | ``` 87 | 88 | is printed. 89 | 90 | ### Usage with `import` 91 | 92 | Since it would be tedious to run `check -a` every time you import new music 93 | into beets, _beets-check_ will add checksum automatically. Before file 94 | is imported the plugin will also check the file with the provided 95 | third-party tools. If the check fails beets will ask you to confirm the 96 | import. 97 | 98 | ``` 99 | $ beet import 'Abbey Road' 100 | Tagging: 101 | The Beatles - Abbey Road 102 | URL: 103 | http://musicbrainz.org/release/eca8996a-a637-3259-ba07-d2573c601a1b 104 | (Similarity: 100.0%) (Vinyl, 1969, DE, Apple Records) 105 | Warning: failed to verify integrity 106 | Abbey Road/01 Come Together.mp3: MPEG stream error 107 | Do you want to skip this album? (Y/n) 108 | ``` 109 | 110 | Alteratively, if auto fixing is enabled, 111 | then _beets-check_ will fix the file using the configured tool. 112 | If the fix is successful, then the import will continue as normal. 113 | Otherwise, it will prompt you in a similar way above, 114 | where you can choose to continue or skip the offending items. 115 | 116 | After a track has been added to the database and all modifications to the tags 117 | have been written, beets-check adds the checksums. This is virtually the same as 118 | running `beets check -a` after the import. 119 | 120 | If you run `import` with the `--quiet` flag the importer will skip 121 | files that do not pass third-party tests automatically and log an 122 | error. 123 | 124 | ### Automatic Update 125 | 126 | The [`write`][write] and [`modify`][modify] commands as well as some plugins will 127 | change a file’s content and thus invalidate its checksum. To relieve you from 128 | updating the checksum manually, _beets-check_ will recalculate the checksums of 129 | all the files that were changed. 130 | 131 | ``` 132 | $ beet check -e 'title:A Day in the Life' 133 | ded5...363f */music/life.mp3 134 | 135 | $ beet modify 'artist=The Beatles' title:A Day in the Life' 136 | 137 | $ beet check -e 'title:A Day in the Life' 138 | d942...5a82 */music/life.mp3 139 | ``` 140 | 141 | This is basically equivalent to running `beets check -u QUERY` after a modifying 142 | command. 143 | 144 | To make sure that a file hasn’t changed before beets changes it, the 145 | plugin will verify the checksum before the file is written. If the 146 | check fails, beets will not write the file and issue a warning. 147 | 148 | ``` 149 | $ beet modify 'artist=The Beatles' 'title:A Day in the Life' 150 | could not write /music/life.mp3: checksum did not match value in library 151 | ``` 152 | 153 | ### Usage with `convert` 154 | 155 | The [`convert`][convert] plugin can replace an audio file with a 156 | transcoded version using the `--keep-new` flag. This will invalidate you 157 | checksum, but _beets-check_ knows about this and will update the 158 | checksum automatically. You can disable this behaviour in the plugin 159 | configuration. Note that, at the moment we do not verify the checksum 160 | prior to the conversion, so a corrupted file might go undetected. This 161 | feature is also only available with the master branch of beets 162 | 163 | [beets]: http://beets.readthedocs.org/en/latest 164 | [write]: http://beets.readthedocs.org/en/latest/reference/cli.html#write 165 | [modify]: http://beets.readthedocs.org/en/latest/reference/cli.html#modify 166 | [convert]: http://beets.readthedocs.org/en/latest/plugins/convert.html 167 | 168 | ## CLI Reference 169 | 170 | ``` 171 | beet check [--quiet] 172 | [ --external 173 | | --add 174 | | --update [--force] 175 | | --export 176 | | --fix [--force] 177 | ] [QUERY...] 178 | beet check --list-tools 179 | ``` 180 | 181 | The plugin has subcommands for checking files, running integrity checks, 182 | adding, updating and exporting checksums and listing third-party tools. All but 183 | the last accepty a `QUERY` paramter that will restrict the operation to files 184 | matching the query. Remember, if a query contains a slash beets will 185 | [interpret it as a path][path query] and match all files that are contained in 186 | a subdirectory of that path. 187 | 188 | The default `check` command, as well as the `--add`, `--update`, and 189 | `--external` commands provide structured output to `stderr` to be easily parseable 190 | by other tools. If a file’s checksum cannot be verified the line 191 | `FAILED: /path/to/file` is printed to stdout. If an external test 192 | fails, the line `WARNING error description: /path/to/file` is printed. 193 | 194 | In addition, the commands print a progress indicator to `stdout` if 195 | `stdout` is connected to a terminal. This can be disabled with the 196 | **`-q, --quiet`** flag. 197 | 198 | - **`beet check [-q] [QUERY...]`** The default command verifies all 199 | file checksums against the database. The output is described above. 200 | Exits with status code `15` if at least one file does not pass a 201 | test. 202 | 203 | - **`-e, --external`** Run third-party tools for the given file. The 204 | output is described above. Exits with status code `15` if at least 205 | one file does not pass a test. 206 | 207 | - **`-a, --add`** Look for files in the database that don’t have a 208 | checksum, compute it from the file and add it to the database. This will also 209 | print warnings for failed integrity checks. 210 | 211 | - **`-u, --update`** Calculate checksums for all files matching the 212 | query and write the them to the database. If no query is given this will 213 | overwrite all checksums already in the database. Since that is almost 214 | certainly not what you want, beets will ask you for confirmation in that 215 | case unless the `--force` flag is set. 216 | 217 | - **`--export`** Outputs a list of filenames with corresponding 218 | checksums in the format used by the `sha256sum` command. You can then use 219 | that command to check your files externally. For example 220 | `beet check -e | sha256sum -c`. 221 | 222 | - **`-x, --fix [--force | -f]`** Since `v0.9.2`. Fix files with 223 | third-party tools. Since this changes files it will ask for you to 224 | confirm the fixes. This can be disabled with the `--force` flag. 225 | 226 | - **`-l, --list-tools`** Outputs a list of third party programs that 227 | _beets-check_ uses to verify file integrity and shows whether they are 228 | installed. The plugin comes with support for the 229 | [`oggz-validate`][oggz-validate], [`mp3val`][mp3val] and [`flac`][flac] commands. 230 | 231 | [path query]: http://beets.readthedocs.org/en/latest/reference/query.html#path-queries 232 | [flac]: https://xiph.org/flac/documentation_tools_flac.html 233 | [mp3val]: http://mp3val.sourceforge.net/ 234 | [oggz-validate]: https://www.xiph.org/oggz/ 235 | 236 | ## Configuration 237 | 238 | By default _beets-check_ uses the following configuration. 239 | 240 | ```yaml 241 | check: 242 | import: yes 243 | write-check: yes 244 | write-update: yes 245 | convert-update: yes 246 | integrity: yes 247 | auto-fix: no 248 | threads: num_of_cpus 249 | ``` 250 | 251 | These option control at which point _beets-check_ will be used automatically by 252 | other beets commands. You can disable each option by setting its value to `no`. 253 | 254 | - `import: no` Don’t add checksums for new files during the import process. 255 | This also disables integrity checks on import and will not ask you to skip 256 | the import of corrupted files. 257 | - `write-check: no` Don’t verify checksums before writing files with 258 | `beet write` or `beet modify`. 259 | - `write-update: no` Don’t update checksums after writing files with 260 | `beet write` or `beet modify`. 261 | - `convert-update: no` Don’t updated the checksum if a file has been 262 | converted with the `--keep-new` flag. 263 | - `integrity: no` Don't preform integrity checks on import 264 | - `auto-fix: yes` Automatically try to fix files on import with [third-party tools](#third-party-tools) 265 | - `threads: 4` Use four threads to compute checksums. 266 | 267 | ### Third-party Tools 268 | 269 | _beets-check_ allows you to configure custom tests for your files. 270 | 271 | Custom tests are shell commands that are run on an audio file and 272 | may produce an error. 273 | 274 | ```yaml 275 | check: 276 | tools: 277 | mp3val: 278 | cmd: "mp3val {}" 279 | formats: MP3 280 | error: '^WARNING: .* \(offset 0x[0-9a-f]+\): (.*)$' 281 | fix: "mp3val -f -nb {}" 282 | ``` 283 | 284 | Each tool is a dictionary entry under `check.tools`, where the key is 285 | the tools name and the value is a configuration dictionary with the 286 | following keys. 287 | 288 | - **`cmd`** The shell command that tests the file. The string is 289 | formatted with python’s [`str.format()`][python-format] to replace 290 | '{}' with the quoted path of the file to check. 291 | 292 | - **`formats`** A space separated list of audio formats the tool can 293 | check. Valid formats include 'MP' 294 | 295 | - **`error`** Python regular expression to match against the tools 296 | output. If a match is found, an error is assumed to have occured 297 | and the error description is the first match group. 298 | 299 | - **`fix`** Shell command to run when fixing files. The command is 300 | formtted similar to `cmd`. 301 | 302 | A test run with a given tool is assumed to have failed in one of the 303 | following two cases. 304 | 305 | - The combined output of `stdout` and `stderr` matches the `error` 306 | Regular Expression. 307 | 308 | - The shell command exits with a non-zero status code. 309 | 310 | [python-format]: https://docs.python.org/2/library/string.html#format-string-syntax 311 | 312 | ## License 313 | 314 | Copyright (c) 2014 Thomas Scholtes 315 | 316 | Permission is hereby granted, free of charge, to any person obtaining a 317 | copy of this software and associated documentation files (the "Software"), to 318 | deal in the Software without restriction, including without limitation the 319 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 320 | sell copies of the Software, and to permit persons to whom the Software is 321 | furnished to do so, subject to the following conditions: 322 | 323 | The above copyright notice and this permission notice shall be included in 324 | all copies or substantial portions of the Software. 325 | 326 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 327 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 328 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 329 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 330 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 331 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 332 | SOFTWARE. 333 | -------------------------------------------------------------------------------- /test/cli_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | from unittest import TestCase 5 | 6 | import beets.library 7 | import beets.ui 8 | import pytest 9 | from beets.library import Item 10 | from beets.ui import UserError 11 | 12 | from beetsplug.check import set_checksum, verify_checksum 13 | from test.helper import MockChecker, TestHelper, captureLog, captureStdout, controlStdin 14 | 15 | 16 | class TestBase(TestHelper, TestCase): 17 | def setUp(self): 18 | super().setUp() 19 | self.setupBeets() 20 | 21 | def tearDown(self): 22 | super().tearDown() 23 | 24 | 25 | class CheckAddTest(TestBase, TestCase): 26 | """beet check --add""" 27 | 28 | def test_add_checksums(self): 29 | self.setupFixtureLibrary() 30 | item = self.lib.items().get() 31 | del item["checksum"] 32 | item.store() 33 | 34 | beets.ui._raw_main(["check", "-a"]) 35 | 36 | item = self.lib.items().get() 37 | assert "checksum" in item 38 | 39 | def test_dont_add_existing_checksums(self): 40 | self.setupFixtureLibrary() 41 | item = self.lib.items().get() 42 | set_checksum(item) 43 | orig_checksum = item["checksum"] 44 | 45 | self.modifyFile(item.path) 46 | beets.ui._raw_main(["check", "-a"]) 47 | 48 | item["checksum"] = "" 49 | item.load() 50 | assert item["checksum"] == orig_checksum 51 | 52 | def test_dont_fail_missing_file(self): 53 | self.setupFixtureLibrary() 54 | item = self.lib.items().get() 55 | del item["checksum"] 56 | item.path = "/doesnotexist" 57 | item.store() 58 | 59 | with captureLog() as logs: 60 | beets.ui._raw_main(["check", "-a"]) 61 | 62 | assert "WARNING No such file: /doesnotexist" in "\n".join(logs) 63 | 64 | def test_add_shows_integrity_warning(self): 65 | MockChecker.install() 66 | item = self.addIntegrityFailFixture(checksum=False) 67 | 68 | with captureLog() as logs: 69 | beets.ui._raw_main(["check", "-a"]) 70 | 71 | assert "WARNING file is corrupt: {}".format( 72 | item.path.decode("utf-8") 73 | ) in "\n".join(logs) 74 | 75 | 76 | class CheckTest(TestBase, TestCase): 77 | """beet check""" 78 | 79 | def test_check_success(self): 80 | self.setupFixtureLibrary() 81 | with captureStdout() as stdout: 82 | beets.ui._raw_main(["check"]) 83 | assert ( 84 | stdout.getvalue().split("\n")[-2] == "All checksums successfully verified" 85 | ) 86 | 87 | def test_check_failed_error_log(self): 88 | self.setupFixtureLibrary() 89 | item = self.lib.items().get() 90 | self.modifyFile(item.path) 91 | 92 | try: 93 | with captureLog("beets.check") as logs: 94 | beets.ui._raw_main(["check"]) 95 | assert "FAILED: {}".format(item.path.decode("utf-8")) in "\n".join(logs) 96 | except SystemExit: 97 | pass 98 | 99 | def test_not_found_error_log(self): 100 | self.setupFixtureLibrary() 101 | item = self.lib.items().get() 102 | item.path = "/doesnotexist" 103 | item.store() 104 | 105 | try: 106 | with captureLog("beets.check") as logs: 107 | beets.ui._raw_main(["check"]) 108 | assert "OK:" in "\n".join(logs) 109 | assert "ERROR [Errno 2] No such file or directory" in "\n".join(logs) 110 | except SystemExit: 111 | pass 112 | 113 | def test_check_failed_exit_code(self): 114 | self.setupFixtureLibrary() 115 | item = self.lib.items().get() 116 | self.modifyFile(item.path) 117 | 118 | with pytest.raises(SystemExit) as exc_info: 119 | beets.ui._raw_main(["check"]) 120 | assert exc_info.value.code == 15 121 | 122 | 123 | class CheckIntegrityTest(TestBase, TestCase): 124 | # TODO beet check --external=mp3val,other 125 | """beet check --external""" 126 | 127 | def test_integrity_warning(self): 128 | MockChecker.install() 129 | self.addIntegrityFailFixture() 130 | 131 | with pytest.raises(SystemExit), captureLog() as logs: 132 | beets.ui._raw_main(["check", "--external"]) 133 | 134 | assert "WARNING file is corrupt" in "\n".join(logs) 135 | 136 | def test_check_failed_exit_code(self): 137 | MockChecker.install() 138 | self.addIntegrityFailFixture() 139 | 140 | with pytest.raises(SystemExit) as exc_info: 141 | beets.ui._raw_main(["check", "--external"]) 142 | assert exc_info.value.code == 15 143 | 144 | def test_no_integrity_checkers_warning(self): 145 | MockChecker.installNone() 146 | self.addIntegrityFailFixture() 147 | 148 | with pytest.raises(UserError) as exc_info: 149 | beets.ui._raw_main(["check", "--external"]) 150 | 151 | assert "No integrity checkers found." in exc_info.value.args[0] 152 | 153 | def test_print_integrity_checkers(self): 154 | MockChecker.install() 155 | self.addIntegrityFailFixture() 156 | 157 | with pytest.raises(SystemExit), captureStdout() as stdout: 158 | beets.ui._raw_main(["check", "--external"]) 159 | 160 | assert "Using integrity checker mock" in stdout.getvalue() 161 | 162 | 163 | class CheckUpdateTest(TestBase, TestCase): 164 | """beet check --update""" 165 | 166 | def test_force_all_update(self): 167 | self.setupFixtureLibrary() 168 | item = self.lib.items().get() 169 | orig_checksum = item["checksum"] 170 | self.modifyFile(item.path) 171 | 172 | beets.ui._raw_main(["check", "--force", "--update"]) 173 | 174 | item = self.lib.items().get() 175 | assert item["checksum"] != orig_checksum 176 | verify_checksum(item) 177 | 178 | def test_update_all_confirmation(self): 179 | self.setupFixtureLibrary() 180 | item = self.lib.items().get() 181 | orig_checksum = item["checksum"] 182 | self.modifyFile(item.path) 183 | 184 | with captureStdout() as stdout, controlStdin("y"): 185 | beets.ui._raw_main(["check", "--update"]) 186 | 187 | assert "Do you want to overwrite all checksums" in stdout.getvalue() 188 | 189 | item = self.lib.items().get() 190 | assert item["checksum"] != orig_checksum 191 | verify_checksum(item) 192 | 193 | def test_update_all_confirmation_no(self): 194 | self.setupFixtureLibrary() 195 | item = self.lib.items().get() 196 | orig_checksum = item["checksum"] 197 | self.modifyFile(item.path) 198 | 199 | with controlStdin("n"): 200 | beets.ui._raw_main(["check", "--update"]) 201 | 202 | item = self.lib.items().get() 203 | assert item["checksum"] == orig_checksum 204 | 205 | def test_update_nonexistent(self): 206 | item = Item(path="/doesnotexist") 207 | self.lib.add(item) 208 | 209 | with captureLog() as logs: 210 | beets.ui._raw_main(["check", "--update", "--force"]) 211 | 212 | assert "ERROR [Errno 2] No such file or directory" in "\n".join(logs) 213 | 214 | 215 | class CheckExportTest(TestBase, TestCase): 216 | """beet check --export""" 217 | 218 | def test_export(self): 219 | self.setupFixtureLibrary() 220 | with captureStdout() as stdout: 221 | beets.ui._raw_main(["check", "--export"]) 222 | 223 | item = self.lib.items().get() 224 | assert ( 225 | "{} *{}\n".format(item.checksum, item.path.decode("utf-8")) 226 | in stdout.getvalue() 227 | ) 228 | 229 | 230 | class IntegrityCheckTest(TestHelper, TestCase): 231 | """beet check --external 232 | 233 | For integrated third-party tools""" 234 | 235 | def setUp(self): 236 | super().setUp() 237 | self.setupBeets() 238 | self.setupFixtureLibrary() 239 | self.enableIntegrityCheckers() 240 | 241 | def tearDown(self): 242 | super().tearDown() 243 | 244 | def test_mp3_integrity(self): 245 | item = self.lib.items(["path::truncated.mp3"]).get() 246 | 247 | with pytest.raises(SystemExit), captureLog() as logs: 248 | beets.ui._raw_main(["check", "--external"]) 249 | assert ( 250 | "check: WARNING It seems that file is " 251 | "truncated or there is garbage at the " 252 | "end of the file: {}".format(item.path.decode("utf-8")) 253 | in logs 254 | ) 255 | 256 | def test_flac_integrity(self): 257 | item = self.lib.items("truncated.flac").get() 258 | 259 | with pytest.raises(SystemExit), captureLog() as logs: 260 | beets.ui._raw_main(["check", "--external"]) 261 | logs = "\n".join(logs) 262 | assert re.search( 263 | f"check: WARNING (while|during) decoding( data)?: {item.path.decode('utf-8')}", 264 | logs, 265 | ) 266 | 267 | def test_ogg_vorbis_integrity(self): 268 | item = self.lib.items("truncated.ogg").get() 269 | 270 | with captureLog() as logs, pytest.raises(SystemExit): 271 | beets.ui._raw_main(["check", "--external"]) 272 | assert ( 273 | f"check: WARNING non-zero exit code for oggz-validate: {str(item.path, 'utf-8')}" 274 | in logs 275 | ) 276 | 277 | def test_shellquote(self): 278 | item = self.lib.items(["ok.flac"]).get() 279 | item["title"] = "ok's" 280 | item.move() 281 | 282 | with captureLog() as logs: 283 | beets.ui._raw_main(["check", "--external", item.title]) 284 | assert "WARNING" not in "\n".join(logs) 285 | 286 | 287 | class FixIntegrityTest(TestHelper, TestCase): 288 | """beet check -x""" 289 | 290 | def setUp(self): 291 | super().setUp() 292 | self.setupBeets() 293 | self.enableIntegrityCheckers() 294 | 295 | def tearDown(self): 296 | super().tearDown() 297 | 298 | def test_fix_integrity(self): 299 | item = self.addIntegrityFailFixture() 300 | 301 | with pytest.raises(SystemExit), captureLog() as logs: 302 | beets.ui._raw_main(["check", "-e"]) 303 | assert "WARNING It seems that file is truncated" in "\n".join(logs) 304 | 305 | with controlStdin("y"), captureLog() as logs: 306 | beets.ui._raw_main(["check", "--fix"]) 307 | assert item.path.decode("utf-8") in "\n".join(logs) 308 | assert "FIXED: {}".format(item.path.decode("utf-8")) in "\n".join(logs) 309 | 310 | with captureLog() as logs: 311 | beets.ui._raw_main(["check", "-e"]) 312 | assert "WARNING It seems that file is truncated" not in "\n".join(logs) 313 | 314 | def test_fix_flac_integrity(self): 315 | item = self.addItemFixture("truncated.flac") 316 | 317 | with pytest.raises(SystemExit), captureLog() as logs: 318 | beets.ui._raw_main(["check", "-e"]) 319 | assert "WARNING" in "\n".join(logs) 320 | 321 | with controlStdin("y"), captureLog() as logs: 322 | beets.ui._raw_main(["check", "--fix"]) 323 | assert item.path.decode("utf-8") in "\n".join(logs) 324 | assert "FIXED: {}".format(item.path.decode("utf-8")) in "\n".join(logs) 325 | 326 | with captureLog() as logs: 327 | beets.ui._raw_main(["check", "-e"]) 328 | assert "WARNING" not in "\n".join(logs) 329 | 330 | def test_fix_without_confirmation(self): 331 | item = self.addIntegrityFailFixture() 332 | 333 | with pytest.raises(SystemExit), captureLog() as logs: 334 | beets.ui._raw_main(["check", "-e"]) 335 | assert "WARNING It seems that file is truncated" in "\n".join(logs) 336 | 337 | with captureLog() as logs: 338 | beets.ui._raw_main(["check", "--fix", "--force"]) 339 | assert item.path.decode("utf-8") in "\n".join(logs) 340 | 341 | with captureLog() as logs: 342 | beets.ui._raw_main(["check", "-e"]) 343 | assert "WARNING It seems that file is truncated" not in "\n".join(logs) 344 | 345 | def test_update_checksum(self): 346 | item = self.addIntegrityFailFixture() 347 | old_checksum = item["checksum"] 348 | beets.ui._raw_main(["check", "--fix", "--force"]) 349 | 350 | item["checksum"] = "" 351 | item.load() 352 | verify_checksum(item) 353 | assert old_checksum != item["checksum"] 354 | 355 | def test_dont_fix_with_wrong_checksum(self): 356 | item = self.addIntegrityFailFixture() 357 | item["checksum"] = "this is wrong" 358 | item.store() 359 | 360 | with captureLog() as logs: 361 | beets.ui._raw_main(["check", "--fix", "--force"]) 362 | assert "FAILED checksum" in "\n".join(logs) 363 | 364 | item["checksum"] = "" 365 | item.load() 366 | assert item["checksum"] == "this is wrong" 367 | 368 | def test_nothing_to_fix(self): 369 | self.addItemFixture("ok.ogg") 370 | with captureStdout() as stdout: 371 | beets.ui._raw_main(["check", "--fix", "--force"]) 372 | assert "No MP3 files to fix" in stdout.getvalue() 373 | 374 | def test_do_not_fix(self): 375 | item = self.addIntegrityFailFixture() 376 | with controlStdin("n"): 377 | beets.ui._raw_main(["check", "--fix"]) 378 | verify_checksum(item) 379 | 380 | 381 | class ToolListTest(TestHelper, TestCase): 382 | def setUp(self): 383 | super().setUp() 384 | self.enableIntegrityCheckers() 385 | self.setupBeets() 386 | self.orig_path = os.environ["PATH"] 387 | os.environ["PATH"] = self.temp_dir 388 | 389 | def tearDown(self): 390 | super().tearDown() 391 | os.environ["PATH"] = self.orig_path 392 | 393 | def test_list(self): 394 | with captureStdout() as stdout: 395 | beets.ui._raw_main(["check", "--list-tools"]) 396 | assert "mp3val" in stdout.getvalue() 397 | assert "flac" in stdout.getvalue() 398 | assert "oggz-validate" in stdout.getvalue() 399 | 400 | def test_found_mp3val(self): 401 | shutil.copy("/bin/echo", os.path.join(self.temp_dir, "mp3val")) 402 | with captureStdout() as stdout: 403 | beets.ui._raw_main(["check", "--list-tools"]) 404 | assert re.search(r"mp3val *found", stdout.getvalue()) 405 | 406 | def test_oggz_validate_not_found(self): 407 | with captureStdout() as stdout: 408 | beets.ui._raw_main(["check", "--list-tools"]) 409 | assert re.search(r"oggz-validate *not found", stdout.getvalue()) 410 | -------------------------------------------------------------------------------- /beetsplug/check.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Thomas Scholtes 2 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a 4 | # copy of this software and associated documentation files (the "Software"), to 5 | # deal in the Software without restriction, including without limitation the 6 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | # sell copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | 13 | 14 | import os 15 | import re 16 | import shutil 17 | import sys 18 | from collections.abc import MutableSequence 19 | from concurrent import futures 20 | from hashlib import sha256 21 | from optparse import OptionParser 22 | from subprocess import PIPE, STDOUT, Popen, check_call 23 | 24 | import beets 25 | from beets import config, logging 26 | from beets.library import Item, ReadError 27 | from beets.plugins import BeetsPlugin 28 | from beets.ui import Subcommand, UserError, colorize, decargs, input_yn 29 | from beets.util import displayable_path, syspath 30 | 31 | try: 32 | from beets.importer import Action as ImporterAction 33 | except ImportError: 34 | # beets<2.4 compatibility 35 | from beets.importer import action as ImporterAction 36 | 37 | log = logging.getLogger("beets.check") 38 | 39 | 40 | def set_checksum(item): 41 | item["checksum"] = compute_checksum(item) 42 | item.store() 43 | 44 | 45 | def compute_checksum(item): 46 | hash = sha256() 47 | with open(syspath(item.path), "rb") as file: # noqa: FURB101 48 | hash.update(file.read()) 49 | return hash.hexdigest() 50 | 51 | 52 | def verify_checksum(item): 53 | if item["checksum"] != compute_checksum(item): 54 | raise ChecksumError(item.path, "checksum did not match value in library.") 55 | 56 | 57 | def verify_integrity(item): 58 | for checker in IntegrityChecker.allAvailable(): 59 | checker.check(item) 60 | 61 | 62 | class ChecksumError(ReadError): 63 | def __str__(self): 64 | return f"error reading {displayable_path(self.path)}: {self.reason}" 65 | 66 | 67 | class CheckPlugin(BeetsPlugin): 68 | def __init__(self): 69 | super().__init__() 70 | self.config.add({ 71 | "import": True, 72 | "write-check": True, 73 | "write-update": True, 74 | "auto-fix": False, 75 | "integrity": True, 76 | "convert-update": True, 77 | "threads": os.cpu_count(), 78 | "external": { 79 | "mp3val": { 80 | "cmdline": "mp3val {0}", 81 | "formats": "MP3", 82 | "error": r"^WARNING: .* \(offset 0x[0-9a-f]+\): (.*)$", 83 | "fix": "mp3val -nb -f {0}", 84 | }, 85 | "flac": { 86 | # More aggressive check by default 87 | "cmdline": "flac --test --silent --warnings-as-errors {0}", 88 | "formats": "FLAC", 89 | "error": "^.*: (?:WARNING|ERROR),? (.*)$", 90 | # Recodes and fixes errors 91 | "fix": "flac -VFf --preserve-modtime -o {0} {0}", 92 | }, 93 | "oggz-validate": {"cmdline": "oggz-validate {0}", "formats": "OGG"}, 94 | }, 95 | }) 96 | 97 | if self.config["import"]: 98 | self.register_listener("item_imported", self.item_imported) 99 | self.import_stages = [self.copy_original_checksum] 100 | self.register_listener("album_imported", self.album_imported) 101 | if self.config["write-check"]: 102 | self.register_listener("write", self.item_before_write) 103 | if self.config["write-update"]: 104 | self.register_listener("after_write", self.item_after_write) 105 | if self.config["convert-update"]: 106 | self.register_listener("after_convert", self.after_convert) 107 | if self.config["integrity"]: 108 | self.register_listener("import_task_choice", self.verify_import_integrity) 109 | 110 | def commands(self): 111 | return [CheckCommand(self.config)] 112 | 113 | def album_imported(self, lib, album): 114 | for item in album.items(): 115 | if not item.get("checksum", None): 116 | set_checksum(item) 117 | 118 | def item_imported(self, lib, item): 119 | if not item.get("checksum", None): 120 | set_checksum(item) 121 | 122 | def item_before_write(self, item, path, **kwargs): 123 | if path != item.path: 124 | return 125 | if item.get("checksum", None): 126 | verify_checksum(item) 127 | 128 | def item_after_write(self, item, path, **kwargs): 129 | if path != item.path: 130 | return 131 | set_checksum(item) 132 | 133 | def after_convert(self, item, dest, keepnew): 134 | if keepnew: 135 | set_checksum(item) 136 | 137 | def copy_original_checksum(self, config, task): 138 | for item in task.imported_items(): 139 | checksum = None 140 | for replaced in task.replaced_items[item]: 141 | try: 142 | checksum = replaced["checksum"] 143 | except KeyError: 144 | continue 145 | if checksum: 146 | break 147 | if checksum: 148 | item["checksum"] = checksum 149 | item.store() 150 | 151 | def verify_import_integrity(self, session, task): 152 | failed_items: MutableSequence[tuple[IntegrityError, Item]] = [] 153 | if not task.items: 154 | return 155 | for item in task.items: 156 | try: 157 | verify_integrity(item) 158 | except IntegrityError as ex: 159 | failed_items.append((ex, item)) 160 | 161 | if not failed_items: 162 | return 163 | 164 | has_unfixable_errors: bool = False 165 | log.warning("Warning: failed to verify integrity") 166 | for error, item in failed_items: 167 | log.warning(f" {displayable_path(item.path)}: {error}") 168 | if not self.config["auto-fix"]: 169 | has_unfixable_errors = True 170 | continue 171 | 172 | checker = IntegrityChecker.fixer(item) 173 | if not checker: 174 | has_unfixable_errors = True 175 | continue 176 | log.info(f"Fixing file: {displayable_path(item.path)}") 177 | try: 178 | checker.fix(item) 179 | except Exception as e: 180 | log.error(f"Failed to fix {displayable_path(item.path)}: {e}") 181 | has_unfixable_errors = True 182 | item["checksum"] = compute_checksum(item) 183 | 184 | if not has_unfixable_errors: 185 | return 186 | 187 | if beets.config["import"]["quiet"] or input_yn( 188 | "Do you want to skip this album (Y/n)" 189 | ): 190 | log.info("Skipping.") 191 | task.choice_flag = ImporterAction.SKIP 192 | 193 | 194 | class CheckCommand(Subcommand): 195 | def __init__(self, config): 196 | self.threads = config["threads"].get(int) 197 | self.check_integrity = config["integrity"].get(bool) 198 | 199 | parser = OptionParser(usage="%prog [options] [QUERY...]") 200 | parser.add_option( 201 | "-e", 202 | "--external", 203 | action="store_true", 204 | dest="external", 205 | default=False, 206 | help="run external tools", 207 | ) 208 | parser.add_option( 209 | "-a", 210 | "--add", 211 | action="store_true", 212 | dest="add", 213 | default=False, 214 | help="add checksum for all files that do not already have one", 215 | ) 216 | parser.add_option( 217 | "-u", 218 | "--update", 219 | action="store_true", 220 | dest="update", 221 | default=False, 222 | help="compute new checksums and add the to the database", 223 | ) 224 | parser.add_option( 225 | "-f", 226 | "--force", 227 | action="store_true", 228 | dest="force", 229 | default=False, 230 | help="force updating the whole library or fixing all files", 231 | ) 232 | parser.add_option( 233 | "--export", 234 | action="store_true", 235 | dest="export", 236 | default=False, 237 | help="print paths and corresponding checksum", 238 | ) 239 | parser.add_option( 240 | "-x", 241 | "--fix", 242 | action="store_true", 243 | dest="fix", 244 | default=False, 245 | help="fix errors with external tools", 246 | ) 247 | parser.add_option( 248 | "-l", 249 | "--list-tools", 250 | action="store_true", 251 | dest="list_tools", 252 | default=False, 253 | help="list available third-party used to check integrity", 254 | ) 255 | parser.add_option( 256 | "-q", 257 | "--quiet", 258 | action="store_true", 259 | dest="quiet", 260 | default=False, 261 | help="only show errors", 262 | ) 263 | super().__init__( 264 | parser=parser, name="check", help="compute and verify checksums" 265 | ) 266 | 267 | def func(self, lib, options, arguments): 268 | self.quiet = options.quiet 269 | self.lib = lib 270 | arguments = decargs(arguments) 271 | self.query = arguments 272 | self.force_update = options.force 273 | if options.add: 274 | self.add() 275 | elif options.update: 276 | self.update() 277 | elif options.export: 278 | self.export() 279 | elif options.fix: 280 | self.fix(ask=not options.force) 281 | elif options.list_tools: 282 | self.list_tools() 283 | else: 284 | self.check(options.external) 285 | 286 | def add(self): 287 | self.log("Looking for files without checksums...") 288 | items = [i for i in self.lib.items(self.query) if not i.get("checksum", None)] 289 | 290 | def add(item): 291 | log.debug(f"adding checksum for {displayable_path(item.path)}") 292 | try: 293 | set_checksum(item) 294 | except FileNotFoundError: 295 | log.warning( 296 | "{} {}: {}".format( 297 | colorize("text_warning", "WARNING"), 298 | "No such file", 299 | displayable_path(item.path), 300 | ) 301 | ) 302 | return 303 | if self.check_integrity: 304 | try: 305 | verify_integrity(item) 306 | except IntegrityError as ex: 307 | log.warning( 308 | "{} {}: {}".format( 309 | colorize("text_warning", "WARNING"), 310 | ex.reason, 311 | displayable_path(item.path), 312 | ) 313 | ) 314 | 315 | self.execute_with_progress(add, items, msg="Adding missing checksums") 316 | 317 | def check(self, external): 318 | if external and not IntegrityChecker.allAvailable(): 319 | no_checkers_warning = ( 320 | "No integrity checkers found. Run 'beet check --list-tools'" 321 | ) 322 | raise UserError(no_checkers_warning) 323 | 324 | if external: 325 | progs = [c.name for c in IntegrityChecker.allAvailable()] 326 | plural = "s" if len(progs) > 1 else "" 327 | self.log("Using integrity checker{} {}".format(plural, ", ".join(progs))) 328 | 329 | items = list(self.lib.items(self.query)) 330 | failures = [0] 331 | 332 | def check(item): 333 | try: 334 | if external: 335 | verify_integrity(item) 336 | elif item.get("checksum", None): 337 | verify_checksum(item) 338 | log.debug( 339 | "{}: {}".format( 340 | colorize("text_success", "OK"), displayable_path(item.path) 341 | ) 342 | ) 343 | except ChecksumError: 344 | log.error( 345 | "{}: {}".format( 346 | colorize("text_error", "FAILED"), displayable_path(item.path) 347 | ) 348 | ) 349 | failures[0] += 1 350 | except IntegrityError as ex: 351 | log.warning( 352 | "{} {}: {}".format( 353 | colorize("text_warning", "WARNING"), 354 | ex.reason, 355 | displayable_path(item.path), 356 | ) 357 | ) 358 | failures[0] += 1 359 | except OSError as exc: 360 | log.error("{} {}".format(colorize("text_error", "ERROR"), exc)) 361 | failures[0] += 1 362 | 363 | if external: 364 | msg = "Running external tests" 365 | else: 366 | msg = "Verifying checksums" 367 | self.execute_with_progress(check, items, msg) 368 | 369 | failures = failures[0] 370 | if external: 371 | if failures: 372 | self.log(f"Found {failures} integrity error(s)") 373 | sys.exit(15) 374 | else: 375 | self.log("Integrity successfully verified") 376 | else: 377 | if failures: 378 | self.log(f"Failed to verify checksum of {failures} file(s)") 379 | sys.exit(15) 380 | else: 381 | self.log("All checksums successfully verified") 382 | 383 | def update(self): 384 | if ( 385 | not self.query 386 | and not self.force_update 387 | and not input_yn( 388 | "Do you want to overwrite all checksums in your database? (y/n)", 389 | require=True, 390 | ) 391 | ): 392 | return 393 | 394 | items = self.lib.items(self.query) 395 | 396 | def update(item): 397 | log.debug(f"updating checksum: {displayable_path(item.path)}") 398 | try: 399 | set_checksum(item) 400 | except OSError as exc: 401 | log.error("{} {}".format(colorize("text_error", "ERROR"), exc)) 402 | 403 | self.execute_with_progress(update, items, msg="Updating checksums") 404 | 405 | def export(self): 406 | for item in self.lib.items(self.query): 407 | if item.get("checksum", None): 408 | print(f"{item.checksum} *{displayable_path(item.path)}") # noqa: T201 409 | 410 | def fix(self, ask=True): 411 | items = list(self.lib.items(self.query)) 412 | failed = [] 413 | 414 | def check(item): 415 | try: 416 | if "checksum" in item: 417 | verify_checksum(item) 418 | fixer = IntegrityChecker.fixer(item) 419 | if fixer: 420 | fixer.check(item) 421 | log.debug( 422 | "{}: {}".format( 423 | colorize("text_success", "OK"), displayable_path(item.path) 424 | ) 425 | ) 426 | except IntegrityError: 427 | failed.append(item) 428 | except ChecksumError: 429 | log.error( 430 | "{}: {}".format( 431 | colorize("text_error", "FAILED checksum"), 432 | displayable_path(item.path), 433 | ) 434 | ) 435 | except OSError as exc: 436 | log.error("{} {}".format(colorize("text_error", "ERROR"), exc)) 437 | 438 | self.execute_with_progress(check, items, msg="Verifying integrity") 439 | 440 | if not failed: 441 | self.log("No MP3 files to fix") 442 | return 443 | 444 | for item in failed: 445 | log.info(displayable_path(item.path)) 446 | 447 | if ask and not input_yn( 448 | "Do you want to fix these files? {} (y/n)", require=True 449 | ): 450 | return 451 | 452 | def fix(item): 453 | fixer = IntegrityChecker.fixer(item) 454 | if fixer: 455 | fixer.fix(item) 456 | log.debug( 457 | "{}: {}".format( 458 | colorize("text_success", "FIXED"), displayable_path(item.path) 459 | ) 460 | ) 461 | set_checksum(item) 462 | 463 | self.execute_with_progress(fix, failed, msg="Fixing files") 464 | 465 | def list_tools(self): 466 | checkers = [ 467 | (checker.name, checker.available()) for checker in IntegrityChecker.all() 468 | ] 469 | prog_length = max(len(c[0]) for c in checkers) + 3 470 | for name, available in checkers: 471 | msg = name + (prog_length - len(name)) * " " 472 | if available: 473 | msg += colorize("text_success", "found") 474 | else: 475 | msg += colorize("text_error", "not found") 476 | print(msg) # noqa: T201 477 | 478 | def log(self, msg): 479 | if not self.quiet: 480 | print(msg) # noqa: T201 481 | 482 | def log_progress(self, msg, index, total): 483 | if self.quiet or not sys.stdout.isatty(): 484 | return 485 | msg = f"{msg}: {index}/{total} [{index * 100 / total}%]" 486 | sys.stdout.write(msg + "\r") 487 | sys.stdout.flush() 488 | if index == total: 489 | sys.stdout.write("\n") 490 | else: 491 | sys.stdout.write(len(msg) * " " + "\r") 492 | 493 | def execute_with_progress(self, func, args, msg=None): 494 | """Run `func` for each value in the iterator `args` in a thread pool. 495 | 496 | When the function has finished it logs the progress and the `msg`. 497 | """ 498 | total = len(args) 499 | finished = 0 500 | with futures.ThreadPoolExecutor(max_workers=self.threads) as e: 501 | for _ in e.map(func, args): 502 | finished += 1 503 | self.log_progress(msg, finished, total) 504 | 505 | 506 | class IntegrityError(ReadError): 507 | def __str__(self): 508 | return f"error reading {displayable_path(self.path)}: {self.reason}" 509 | 510 | 511 | class IntegrityChecker: 512 | @classmethod 513 | def all(cls): 514 | if hasattr(cls, "_all"): 515 | return cls._all 516 | 517 | cls._all = [] 518 | for name, tool in config["check"]["external"].items(): 519 | cls._all.append(cls(name, tool)) 520 | return cls._all 521 | 522 | @classmethod 523 | def allAvailable(cls): 524 | if not hasattr(cls, "_all_available"): 525 | cls._all_available = [c for c in cls.all() if c.available()] 526 | return cls._all_available 527 | 528 | def __init__(self, name, config): 529 | self.name = name 530 | self.cmdline = config["cmdline"].get(str) 531 | 532 | if config["formats"].exists(): 533 | self.formats = config["formats"].as_str_seq() 534 | else: 535 | self.formats = True 536 | 537 | if config["error"].exists(): 538 | self.error_match = re.compile(config["error"].get(str), re.MULTILINE) 539 | else: 540 | self.error_match = False 541 | 542 | if config["fix"].exists(): 543 | self.fixcmd = config["fix"].get(str) 544 | else: 545 | self.fixcmd = False 546 | 547 | def available(self) -> bool: 548 | return shutil.which(self.cmdline.split(" ")[0]) is not None 549 | 550 | @classmethod 551 | def fixer(cls, item): 552 | """Return an `IntegrityChecker` instance that can fix this item.""" 553 | for checker in cls.allAvailable(): 554 | if checker.can_fix(item): 555 | return checker 556 | 557 | def can_check(self, item): 558 | return self.formats is True or item.format in self.formats 559 | 560 | def check(self, item): 561 | if not self.can_check(item): 562 | return 563 | process = Popen( 564 | self.cmdline.format(self.shellquote(item.path.decode("utf-8"))), 565 | shell=True, 566 | stdin=PIPE, 567 | stdout=PIPE, 568 | stderr=STDOUT, 569 | ) 570 | stdout = process.communicate()[0] 571 | if self.error_match: 572 | match = self.error_match.search(stdout.decode("utf-8")) 573 | else: 574 | match = False 575 | if match: 576 | raise IntegrityError(item.path, match.group(1)) 577 | elif process.returncode: 578 | raise IntegrityError(item.path, f"non-zero exit code for {self.name}") 579 | 580 | def can_fix(self, item): 581 | return self.can_check(item) and self.fixcmd 582 | 583 | def fix(self, item): 584 | assert isinstance(self.fixcmd, str) 585 | check_call( 586 | self.fixcmd.format(self.shellquote(item.path.decode("utf-8"))), 587 | shell=True, 588 | stdin=PIPE, 589 | stdout=PIPE, 590 | stderr=STDOUT, 591 | ) 592 | 593 | def shellquote(self, s): 594 | return "'" + s.replace("'", r"'\''") + "'" 595 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.10" 4 | resolution-markers = [ 5 | "python_full_version >= '3.11'", 6 | "python_full_version < '3.11'", 7 | ] 8 | 9 | [[package]] 10 | name = "beets" 11 | version = "2.5.1" 12 | source = { registry = "https://pypi.org/simple" } 13 | dependencies = [ 14 | { name = "colorama", marker = "sys_platform == 'win32'" }, 15 | { name = "confuse" }, 16 | { name = "jellyfish" }, 17 | { name = "lap" }, 18 | { name = "mediafile" }, 19 | { name = "musicbrainzngs" }, 20 | { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, 21 | { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, 22 | { name = "platformdirs" }, 23 | { name = "pyyaml" }, 24 | { name = "typing-extensions" }, 25 | { name = "unidecode" }, 26 | ] 27 | sdist = { url = "https://files.pythonhosted.org/packages/c9/32/2b5ae0038c442e783b4f00b4145b15357a2f2358fd985c60a1f890751bb0/beets-2.5.1.tar.gz", hash = "sha256:7feefd70804fbcf26516089f472bac34c6a77e8e20ec539252fd1bafc91de9a2", size = 2147257, upload-time = "2025-10-14T22:52:55.631Z" } 28 | wheels = [ 29 | { url = "https://files.pythonhosted.org/packages/62/26/c459ae5217a69d1a2c83ddb80b61480764e990049b7d9f6a5b82660457f4/beets-2.5.1-py3-none-any.whl", hash = "sha256:3e58f33d898d007e6bfd385bd145d2c39325ef6b6f831f7269d037bbcb542bf7", size = 573677, upload-time = "2025-10-14T22:52:53.728Z" }, 30 | ] 31 | 32 | [[package]] 33 | name = "beets-check" 34 | version = "0.15.3" 35 | source = { editable = "." } 36 | dependencies = [ 37 | { name = "beets" }, 38 | { name = "mediafile" }, 39 | ] 40 | 41 | [package.dev-dependencies] 42 | dev = [ 43 | { name = "pytest" }, 44 | { name = "ruff" }, 45 | ] 46 | 47 | [package.metadata] 48 | requires-dist = [ 49 | { name = "beets", specifier = ">=2,<3" }, 50 | { name = "mediafile", specifier = ">=0.13.0" }, 51 | ] 52 | 53 | [package.metadata.requires-dev] 54 | dev = [ 55 | { name = "pytest", specifier = ">=8" }, 56 | { name = "ruff", specifier = ">=0.14" }, 57 | ] 58 | 59 | [[package]] 60 | name = "colorama" 61 | version = "0.4.6" 62 | source = { registry = "https://pypi.org/simple" } 63 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 64 | wheels = [ 65 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 66 | ] 67 | 68 | [[package]] 69 | name = "confuse" 70 | version = "2.0.1" 71 | source = { registry = "https://pypi.org/simple" } 72 | dependencies = [ 73 | { name = "pyyaml" }, 74 | ] 75 | sdist = { url = "https://files.pythonhosted.org/packages/a7/77/05e2284baff5f2106f74b528b9930caf764d6c400733eb42e617c4234a7d/confuse-2.0.1.tar.gz", hash = "sha256:7379a2ad49aaa862b79600cc070260c1b7974d349f4fa5e01f9afa6c4dd0611f", size = 50872, upload-time = "2023-04-01T23:26:58.031Z" } 76 | wheels = [ 77 | { url = "https://files.pythonhosted.org/packages/32/1f/cf496479814d41fc252004482deeb90b740b4a6a391a3355c0b11d7e0abf/confuse-2.0.1-py3-none-any.whl", hash = "sha256:9b9e5bbc70e2cb9b318bcab14d917ec88e21bf1b724365e3815eb16e37aabd2a", size = 24750, upload-time = "2023-04-01T23:26:55.321Z" }, 78 | ] 79 | 80 | [[package]] 81 | name = "exceptiongroup" 82 | version = "1.2.2" 83 | source = { registry = "https://pypi.org/simple" } 84 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } 85 | wheels = [ 86 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, 87 | ] 88 | 89 | [[package]] 90 | name = "filetype" 91 | version = "1.2.0" 92 | source = { registry = "https://pypi.org/simple" } 93 | sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } 94 | wheels = [ 95 | { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, 96 | ] 97 | 98 | [[package]] 99 | name = "iniconfig" 100 | version = "2.0.0" 101 | source = { registry = "https://pypi.org/simple" } 102 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } 103 | wheels = [ 104 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, 105 | ] 106 | 107 | [[package]] 108 | name = "jellyfish" 109 | version = "1.1.0" 110 | source = { registry = "https://pypi.org/simple" } 111 | sdist = { url = "https://files.pythonhosted.org/packages/c9/2f/cda51a742a873ae4b0b52620cd282a885195612edaa2a7ec4b68cf968b2d/jellyfish-1.1.0.tar.gz", hash = "sha256:2a2eec494c81dc1eb23dfef543110dad1873538eccaffabea8520bdac8aecbc1", size = 364391, upload-time = "2024-07-28T08:20:50.882Z" } 112 | wheels = [ 113 | { url = "https://files.pythonhosted.org/packages/ce/60/364265ea5ac6fab5a6f6645c2ad3a0d5c41b1b0ce2b0e5f98ce2cfc67ac2/jellyfish-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:feb1fa5838f2bb6dbc9f6d07dabf4b9d91e130b289d72bd70dc33b651667688f", size = 306835, upload-time = "2024-07-28T08:19:05.263Z" }, 114 | { url = "https://files.pythonhosted.org/packages/47/16/fafbbb45fca910df4df2bceea6f99265f59667348230b8775cfdbf1d0415/jellyfish-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:623fa58cca9b8e594a46e7b9cf3af629588a202439d97580a153d6af24736a1b", size = 303195, upload-time = "2024-07-28T08:19:07.215Z" }, 115 | { url = "https://files.pythonhosted.org/packages/f3/bf/ba3807e42dd3622523c719cd7fc03576e905a5dc9937c4c7efb04a809724/jellyfish-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a87e4a17006f7cdd7027a053aeeaacfb0b3366955e242cd5b74bbf882bafe022", size = 346571, upload-time = "2024-07-28T08:19:08.973Z" }, 116 | { url = "https://files.pythonhosted.org/packages/e3/5f/b85a71bcca1bced0751e9a132604792b73e33b2d51c7b5506b74db1720c5/jellyfish-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f10fa36491840bda29f2164cc49e61244ea27c5db5a66aaa437724f5626f5610", size = 343856, upload-time = "2024-07-28T08:19:10.191Z" }, 117 | { url = "https://files.pythonhosted.org/packages/81/00/861186a78604c03bcd7846e4c3f1eed1af9072769f4a60997adccb846e00/jellyfish-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24f91daaa515284cdb691b1e01b0f91f9c9e51e685420725a1ded4f54d5376ff", size = 335971, upload-time = "2024-07-28T08:19:11.934Z" }, 118 | { url = "https://files.pythonhosted.org/packages/06/02/8a21858097971c0779952cdf1a5ea1e65585d123c4d00977b6bebc4305d0/jellyfish-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:65e58350618ebb1488246998a7356a8c9a7c839ec3ecfe936df55be6776fc173", size = 525166, upload-time = "2024-07-28T08:19:13.645Z" }, 119 | { url = "https://files.pythonhosted.org/packages/c4/11/ca9fd25ba78e29c3610e01b2e4ac7218aa41de64f6d3d205dba9b1c530d1/jellyfish-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5c5ed62b23093b11de130c3fe1b381a2d3bfaf086757fa21341ac6f30a353e92", size = 529630, upload-time = "2024-07-28T08:19:14.854Z" }, 120 | { url = "https://files.pythonhosted.org/packages/6f/cb/978a74aa3ae100c63baaaaab32ffc2ef7328c8af024ccc938f3225a65bf0/jellyfish-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c42aa02e791d3e5a8fc6a96bec9f64ebbb2afef27b01eca201b56132e3d0c64e", size = 506763, upload-time = "2024-07-28T08:19:16.168Z" }, 121 | { url = "https://files.pythonhosted.org/packages/ae/23/dd2eba6f8a5724bc651db0bd47ac6f69fd996fa77afbb5e7d6babaaf64fc/jellyfish-1.1.0-cp310-none-win32.whl", hash = "sha256:84680353261161c627cbdd622ea4243e3d3da75894bfacc2f3fcbbe56e8e59d4", size = 201398, upload-time = "2024-07-28T08:19:17.582Z" }, 122 | { url = "https://files.pythonhosted.org/packages/7e/10/1b41958cfef740dfda441e04e725381c7c234a37c1b7da0e768f0d896969/jellyfish-1.1.0-cp310-none-win_amd64.whl", hash = "sha256:017c794b89d827d0306cb056fc5fbd040ff558a90ff0e68a6b60d6e6ba661fe3", size = 207275, upload-time = "2024-07-28T08:19:18.972Z" }, 123 | { url = "https://files.pythonhosted.org/packages/89/e1/d80963467a1b4edd79b46f305bbc060dcffbcb7b138e308ecba86d99ffd1/jellyfish-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fed2e4ecf9b4995d2aa771453d0a0fdf47a5e1b13dbd74b98a30cb0070ede30c", size = 306851, upload-time = "2024-07-28T08:19:20.48Z" }, 124 | { url = "https://files.pythonhosted.org/packages/3b/0c/3df520c610785a83411237eeefbb9ba283eabbee80d2b87b1b93abe5d158/jellyfish-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61a382ba8a3d3cd0bd50029062d54d3a0726679be248789fef6a3901eee47a60", size = 303132, upload-time = "2024-07-28T08:19:21.839Z" }, 125 | { url = "https://files.pythonhosted.org/packages/a2/38/89f737cfa3c9716c583a5aac018b136d69322706cb34d3fca2c741e90533/jellyfish-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a4b526ed2080b97431454075c46c19baddc944e95cc605248e32a2a07be231e", size = 346485, upload-time = "2024-07-28T08:19:23.566Z" }, 126 | { url = "https://files.pythonhosted.org/packages/82/fe/ff9dcf224c4fbb12cf7b6c6ec6f4fc2975f1dcf49a605ef4edd334e170c8/jellyfish-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0fa7450c3217724b73099cb18ee594926fcbc1cc4d9964350f31a4c1dc267b35", size = 343871, upload-time = "2024-07-28T08:19:24.944Z" }, 127 | { url = "https://files.pythonhosted.org/packages/6e/d2/2055b8b6d1f4e432e40cfa44ae3e315e36e4eddb989eed4db7a53bff51e3/jellyfish-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ebb6e9647d5d52f4d461a163449f6d1c73f1a80ccbe98bb17efac0062a6423", size = 335967, upload-time = "2024-07-28T08:19:27.339Z" }, 128 | { url = "https://files.pythonhosted.org/packages/ae/49/a94610285aa756f80844c926ccd51cf0d1b83a7ea00cb56efeb3b96f0606/jellyfish-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:759172602343115f910d7c63b39239051e32425115bc31ab4dafdaf6177f880c", size = 525034, upload-time = "2024-07-28T08:19:28.769Z" }, 129 | { url = "https://files.pythonhosted.org/packages/e7/08/f4472b5f7c2063b63904b95ccc57b5213ca1cfeab4ec7ec7dd3f556a0c7f/jellyfish-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:273fdc362ccdb09259eec9bc4abdc2467d9a54bd94d05ae22e71423dd1357255", size = 529613, upload-time = "2024-07-28T08:19:30.331Z" }, 130 | { url = "https://files.pythonhosted.org/packages/7e/2a/3dd192bb8b731c7618797a1abb24f485f341f4d5d13441a01ddd028d2c33/jellyfish-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bd5c335f8d762447691dc0572f4eaf0cfdfbfffb6dce740341425ab1b32134ff", size = 506597, upload-time = "2024-07-28T08:19:31.798Z" }, 131 | { url = "https://files.pythonhosted.org/packages/dd/9b/556bb5aff966e003441b15b66fe3638a85d70ff66b0be777852d6b71613f/jellyfish-1.1.0-cp311-none-win32.whl", hash = "sha256:cc16a60a42f1541ad9c13c72c797107388227f01189aa3c0ec7ee9b939e57ea8", size = 201447, upload-time = "2024-07-28T08:19:33.433Z" }, 132 | { url = "https://files.pythonhosted.org/packages/cd/76/a077eee8a50c522121ba74b6a6dff6987f1a7333992219251987fe0db850/jellyfish-1.1.0-cp311-none-win_amd64.whl", hash = "sha256:95dfe61eabf360a92e6d76d1c4dbafa29bcb3f70e2ad7354de2661141fcce038", size = 207305, upload-time = "2024-07-28T08:19:34.664Z" }, 133 | { url = "https://files.pythonhosted.org/packages/db/6b/5d66bf5be9ed7b6cb030d111cc4bc4d6d61f19ea6ca4d90c4c6e5bc65382/jellyfish-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:828a7000d369cbd4d812b88510c01fdab20b73dc54c63cdbe03bdff67ab362d0", size = 306412, upload-time = "2024-07-28T08:19:36.185Z" }, 134 | { url = "https://files.pythonhosted.org/packages/cc/b0/cb8bfd5e1a19ff73826971a7656ca59b88e6ae2e5b1d07b7e458a3290c88/jellyfish-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e250dc1074d730a03c96ac9dfce44716cf45e0e2825cbddaf32a015cdf9cf594", size = 302950, upload-time = "2024-07-28T08:19:37.386Z" }, 135 | { url = "https://files.pythonhosted.org/packages/57/51/33fcb48d9a6415838305e2fd787935e086c5bca732987f79c94ba8c696ce/jellyfish-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87dc2a82c45b773a579fb695a5956a54106c1187f27c9ccee8508726d2e59cfc", size = 346287, upload-time = "2024-07-28T08:19:38.646Z" }, 136 | { url = "https://files.pythonhosted.org/packages/4e/f3/ae1d6e474e2ab6a04abf244450424b62c196eb3597885cb7b90d858b3b06/jellyfish-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41677ec860454da5977c698fc64fed73b4054a92c5c62ba7d1af535f8082ac7", size = 343892, upload-time = "2024-07-28T08:19:40.243Z" }, 137 | { url = "https://files.pythonhosted.org/packages/1c/41/91ff9bbf83389fba009de3896ae5f44292242990e53bdbece106eceb33ad/jellyfish-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d4002d01252f18eb26f28b66f6c9ce0696221804d8769553c5912b2f221a18", size = 335763, upload-time = "2024-07-28T08:19:41.673Z" }, 138 | { url = "https://files.pythonhosted.org/packages/33/dd/6a0e7023e0c0aaa1b5a55665062f35bc204cf8dba33816787f383416d23d/jellyfish-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:936df26c10ca6cd6b4f0fb97753087354c568e2129c197cbb4e0f0172db7511f", size = 525046, upload-time = "2024-07-28T08:19:43.101Z" }, 139 | { url = "https://files.pythonhosted.org/packages/96/5d/e37ff36554871d02c9248d3f7837479b0c8f6e92cdeee4e6b017f10e39de/jellyfish-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:684c2093fa0d68a91146e15a1e9ca859259b19d3bc36ec4d60948d86751f744e", size = 529581, upload-time = "2024-07-28T08:19:44.571Z" }, 140 | { url = "https://files.pythonhosted.org/packages/70/25/3a3a39df1c3b28d1e6c0f1693047dec7450d5406ec2c516ac93e35883af5/jellyfish-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fcaefebe9d67f282d89d3a66646b77184a42b3eca2771636789b2dc1288c003", size = 506686, upload-time = "2024-07-28T08:19:46.341Z" }, 141 | { url = "https://files.pythonhosted.org/packages/4e/9c/37c1701ac56c978043239430b69845900632b485573259eac6de015a2ea7/jellyfish-1.1.0-cp312-none-win32.whl", hash = "sha256:e512c99941a257541ffd9f75c7a5c4689de0206841b72f1eb015599d17fed2c3", size = 201172, upload-time = "2024-07-28T08:19:47.562Z" }, 142 | { url = "https://files.pythonhosted.org/packages/de/a5/07ee2c08dcf970284e6412c9b8cb7f79222fc9e2aaf9b3c45837d8e2173b/jellyfish-1.1.0-cp312-none-win_amd64.whl", hash = "sha256:2b928bad2887c662783a4d9b5828ed1fa0e943f680589f7fc002c456fc02e184", size = 206918, upload-time = "2024-07-28T08:19:48.762Z" }, 143 | { url = "https://files.pythonhosted.org/packages/e2/9b/6560e605a057ebb81af8edc1b98923340f8bcf3ebcdfe81ee954f9a66de5/jellyfish-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d510b04e2a39f27aef391ca18bf527ec5d9a2438a63731b87faada83996cb92", size = 314554, upload-time = "2024-10-29T06:37:33.697Z" }, 144 | { url = "https://files.pythonhosted.org/packages/09/a1/95dfec8afa9a2bd895b62a53ef60dfec675bf34ed1f9bd1dae537ca8422b/jellyfish-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57d005cc5daa4d0a8d88341d86b1dce24e3f1d7721da75326c0b7af598a4f58c", size = 310786, upload-time = "2024-10-29T06:37:35.252Z" }, 145 | { url = "https://files.pythonhosted.org/packages/60/d8/7a78797c851e45a3f7cac93d80ec8e49bc33e84193597e450a4079b93708/jellyfish-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889edab0fb2a29d29c148c9327752df525c9bdaef03eef01d1bd9c1f90b47ebf", size = 343421, upload-time = "2024-10-29T06:37:36.747Z" }, 146 | { url = "https://files.pythonhosted.org/packages/79/27/54eada80f74fac303cd041efab5c5631b554f062a5f4aeca5f29179d1dce/jellyfish-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:937b657aacba8fe8482ebc5fea5ba1aee987ecb9da0f037bfb8a1a9045d05746", size = 350009, upload-time = "2024-10-29T06:37:38.196Z" }, 147 | { url = "https://files.pythonhosted.org/packages/5f/7b/76a4f5ef9fdcbe9f4a4e9fc07f79128c3b3c1ab9df3d66a4782c221403fa/jellyfish-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cb5088436ce1fdabcb46aed3a3cc215f0432313596f4e5abe5300ed833b697c", size = 344085, upload-time = "2024-10-29T06:37:39.45Z" }, 148 | { url = "https://files.pythonhosted.org/packages/a7/6a/69daa1863a7d9d03fa314cd3e2f22fd0516d57a54facdf391962802ac962/jellyfish-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:af74156301a0ff05a22e8cf46250678e23fa447279ba6dffbf9feff01128f51d", size = 526959, upload-time = "2024-10-29T06:37:40.911Z" }, 149 | { url = "https://files.pythonhosted.org/packages/81/d9/3f29446d86e6ae6e0faff0507b9e49571ce165d4b86e759cd35f2794cebe/jellyfish-1.1.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3f978bc430bbed4df3c10b2a66be7b5bddd09e6c2856c7a17fa2298fb193d4d4", size = 531998, upload-time = "2024-10-29T06:37:42.062Z" }, 150 | { url = "https://files.pythonhosted.org/packages/0d/c2/7c7a0f167daf4e9135d2786500a348adb8e8c9c733ea4a5a9b425f4fc593/jellyfish-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:b460f0bbde533f6f8624c1d7439e7f511b227ca18a58781e7f38f21961bd3f09", size = 508958, upload-time = "2024-10-29T06:37:43.773Z" }, 151 | { url = "https://files.pythonhosted.org/packages/2e/e4/4d87b377af913633c45a3b65ea2a2113d0acf4194748f8d16c20a4989fb1/jellyfish-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54effec80c7a5013bea8e2ea6cd87fdd35a2c5b35f86ccf69ec33f4212245f25", size = 348149, upload-time = "2024-07-28T08:20:17.943Z" }, 152 | { url = "https://files.pythonhosted.org/packages/67/db/d166a55444bfea3460bc477af96defcdf84b2b045a225c90fa379bcdf0e9/jellyfish-1.1.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12ae67e9016c9a173453023fd7b400ec002bbc106c12722d914c53951acfa190", size = 345503, upload-time = "2024-07-28T08:20:19.618Z" }, 153 | { url = "https://files.pythonhosted.org/packages/c1/66/a9fce02be03110e6259ee07acd4f6b89b6914c9904089c7f2c3d524bf455/jellyfish-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd342f9d4fb0ead8a3c30fe26e442308fb665ca37f4aa97baf448d814469bf1", size = 337608, upload-time = "2024-07-28T08:20:20.97Z" }, 154 | { url = "https://files.pythonhosted.org/packages/da/6b/f392fbd61db0649bb67b66edc3b3fc5defcb944cd1c18f0e7bb784cb5223/jellyfish-1.1.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b0dc9f1bb335b6caa412c3d27028e25d315ef2bc993d425db93e451d7bc28056", size = 527251, upload-time = "2024-07-28T08:20:22.234Z" }, 155 | { url = "https://files.pythonhosted.org/packages/d9/e5/c0662901217afca989b6f6263a6b728a46d213eadbae756d050e0beef7ba/jellyfish-1.1.0-pp310-pypy310_pp73-musllinux_1_1_i686.whl", hash = "sha256:3f12cb59b3266e37ec47bd7c2c37faadc74ae8ccdc0190444daeafda3bd93da2", size = 531325, upload-time = "2024-07-28T08:20:23.843Z" }, 156 | { url = "https://files.pythonhosted.org/packages/9e/10/93ab864ba8c59b4436d5a2260058995b5da22a864d969860b1ade0a7743f/jellyfish-1.1.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c7ea99734b7767243b5b98eca953f0d719b48b0d630af3965638699728ef7523", size = 508647, upload-time = "2024-07-28T08:20:25.252Z" }, 157 | ] 158 | 159 | [[package]] 160 | name = "lap" 161 | version = "0.5.12" 162 | source = { registry = "https://pypi.org/simple" } 163 | dependencies = [ 164 | { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, 165 | { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, 166 | ] 167 | sdist = { url = "https://files.pythonhosted.org/packages/6c/cf/ef745c8977cbb26fba5f8433fd4bfd6bf009a90802c0a1cc7139e11f478b/lap-0.5.12.tar.gz", hash = "sha256:570b414ea7ae6c04bd49d0ec8cdac1dc5634737755784d44e37f9f668bab44fd", size = 1520169, upload-time = "2024-11-30T14:27:56.096Z" } 168 | wheels = [ 169 | { url = "https://files.pythonhosted.org/packages/5b/a7/d66e91ea92628f1e1572db6eb5cd0baa549ef523308f1ce469ea2b380b37/lap-0.5.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c3a38070b24531949e30d7ebc83ca533fcbef6b1d6562f035cae3b44dfbd5ec", size = 1481332, upload-time = "2024-11-30T01:20:54.008Z" }, 170 | { url = "https://files.pythonhosted.org/packages/30/8a/a0e54a284828edc049a1d005fad835e7c8b2d2a563641ec0d3c6fb5ee6d4/lap-0.5.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a301dc9b8a30e41e4121635a0e3d0f6374a08bb9509f618d900e18d209b815c4", size = 1478472, upload-time = "2024-11-30T01:21:10.314Z" }, 171 | { url = "https://files.pythonhosted.org/packages/e8/d6/679d73d2552d0e36c5a2751b6509a62f1fa69d6a2976dac07568498eefde/lap-0.5.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0c1b9ab32c9ba9a94e3f139a0c30141a15fb9e71d69570a6851bbae254c299", size = 1697145, upload-time = "2024-11-30T01:21:47.91Z" }, 172 | { url = "https://files.pythonhosted.org/packages/fa/93/dcfdcd73848c72a0aec5ff587840812764844cdb0b58dd9394e689b8bc09/lap-0.5.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f702e9fbbe3aa265708817ba9d4efb44d52f7013b792c9795f7501ecf269311a", size = 1700582, upload-time = "2024-11-30T01:22:09.43Z" }, 173 | { url = "https://files.pythonhosted.org/packages/dd/1d/66f32e54bbf005fe8483065b3afec4b427f2583df6ae53a2dd540c0f7227/lap-0.5.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9836f034c25b1dfeabd812b7359816911ed05fe55f53e70c30ef849adf07df02", size = 1688038, upload-time = "2024-11-30T01:22:11.863Z" }, 174 | { url = "https://files.pythonhosted.org/packages/a9/1c/faf992abd15b643bd7d70aabcf13ef7544f11ac1167436049a3a0090ce17/lap-0.5.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0416780dbdca2769231a53fb5491bce52775299b014041296a8b5be2d00689df", size = 1697169, upload-time = "2024-11-30T01:22:13.551Z" }, 175 | { url = "https://files.pythonhosted.org/packages/e7/a2/9af5372d383310174f1a9e429da024ae2eaa762e6ee3fc59bdc936a1f6db/lap-0.5.12-cp310-cp310-win_amd64.whl", hash = "sha256:2d6e137e1beb779fcd6a42968feb6a122fdddf72e5b58d865191c31a01ba6804", size = 1477867, upload-time = "2024-11-30T01:22:15.57Z" }, 176 | { url = "https://files.pythonhosted.org/packages/ee/ad/9bb92211ea5b5b43d98f5a57b3e98ccff125ea9bc397f185d5eff1a04260/lap-0.5.12-cp310-cp310-win_arm64.whl", hash = "sha256:a40d52c5511421497ae3f82a5ca85a5442d8776ba2991c6fca146afceea7608f", size = 1467318, upload-time = "2024-11-30T01:22:41.151Z" }, 177 | { url = "https://files.pythonhosted.org/packages/62/ef/bc8bbc34585bcbed2b277d734008480d9ed08a6e3f2de3842ad482484e9c/lap-0.5.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d928652e77bec5a71dc4eb4fb8e15d455253b2a391ca8478ceab7d171cbaec2e", size = 1481210, upload-time = "2024-11-30T01:22:44.992Z" }, 178 | { url = "https://files.pythonhosted.org/packages/ab/81/0d3b31d18bbdcdaab678b461d99688ec3e6a2d2cda2aa9af2ae8ed6910e1/lap-0.5.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4a0ea039fcb2fd388b5e7c1be3402c483d32d3ef8c70261c69ab969ec25cd83", size = 1478370, upload-time = "2024-11-30T01:23:00.354Z" }, 179 | { url = "https://files.pythonhosted.org/packages/3d/90/bd6cff1b6a0c30594a7a2bf94c5f184105e8eb26fa250ce22efdeef58a3a/lap-0.5.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87c0e736c31af0a827dc642132d09c5d4f77d30f5b3f0743b9cd31ef12adb96c", size = 1718144, upload-time = "2024-11-30T01:23:03.345Z" }, 180 | { url = "https://files.pythonhosted.org/packages/7d/d6/97564ef3571cc2a60a6e3ee2f452514b2e549637247cb7de7004e0769864/lap-0.5.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5270141f97027776ced4b6540d51899ff151d8833b5f93f2428de36c2270a9ed", size = 1720027, upload-time = "2024-11-30T01:23:32.025Z" }, 181 | { url = "https://files.pythonhosted.org/packages/3e/7d/73a51aeec1e22257589dad46c724d4d736aa56fdf4c0eff29c06102e21ae/lap-0.5.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:04dc4b44c633051a9942ad60c9ad3da28d7c5f09de93d6054b763c57cbc4ac90", size = 1711923, upload-time = "2024-11-30T01:23:47.213Z" }, 182 | { url = "https://files.pythonhosted.org/packages/86/9c/c1be3d9ebe479beff3d6ee4453908a343c7a388386de28037ff2767debf9/lap-0.5.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:560ec8b9100f78d6111b0acd9ff8805e4315372f23c2dcad2f5f9f8d9c681261", size = 1720922, upload-time = "2024-11-30T01:24:14.228Z" }, 183 | { url = "https://files.pythonhosted.org/packages/cd/4d/18c0c4edadbf9744a02131901c8a856303a901367881e44796a94190b560/lap-0.5.12-cp311-cp311-win_amd64.whl", hash = "sha256:851b9bcc898fa763d6e7c307d681dde199ca969ab00e8292fc13cff34107ea38", size = 1478202, upload-time = "2024-11-30T01:24:29.681Z" }, 184 | { url = "https://files.pythonhosted.org/packages/cc/d2/dcde0db492eb7a2c228e8839e831c6c5fc68f85bea586206405abd2eb44e/lap-0.5.12-cp311-cp311-win_arm64.whl", hash = "sha256:49e14fdbf4d55e7eda6dfd3aba433a91b00d87c7be4dd25059952b871b1e3399", size = 1467411, upload-time = "2024-11-30T01:24:31.92Z" }, 185 | { url = "https://files.pythonhosted.org/packages/24/29/50a77fa27ed19b75b7599defedafd5f4a64a66bdb6255f733fdb8c9fafcb/lap-0.5.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1211fca9d16c0b1383c7a93be2045096ca5e4c306e794fcf777ac52b30f98829", size = 1481435, upload-time = "2024-11-30T01:24:58.094Z" }, 186 | { url = "https://files.pythonhosted.org/packages/c5/2b/41acf93603d3db57e512c77c98f4f71545602efa0574ca685608078cc0f5/lap-0.5.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8dcafbf8363308fb289d7cd3ae9df375ad090dbc2b70f5d7d038832e87d2b1a1", size = 1478195, upload-time = "2024-11-30T01:25:16.925Z" }, 187 | { url = "https://files.pythonhosted.org/packages/3a/6e/d7644b2b2675e2c29cc473c3dde136f02f4ed30ecbc8ef89b51cbb4f7ad1/lap-0.5.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f721ed3fd2b4f6f614870d12aec48bc44c089587930512c3187c51583c811b1c", size = 1725693, upload-time = "2024-11-30T01:25:19.404Z" }, 188 | { url = "https://files.pythonhosted.org/packages/c6/3c/8d3f80135022a2db3eb7212fa9c735b7111dcb149d53deb62357ff2386f0/lap-0.5.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:797d9e14e517ac06337b6dca875bdf9f0d88ec4c3214ebb6d0676fed197dc13f", size = 1726953, upload-time = "2024-11-30T01:25:44.067Z" }, 189 | { url = "https://files.pythonhosted.org/packages/fe/e1/badf139f34ff7c7c07ba55e6f39de9ea443d9b75fd97cc4ed0ce67eeb36b/lap-0.5.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a2424daf7c7afec9b93ed02af921813ab4330826948ce780a25d94ca42df605", size = 1712981, upload-time = "2024-11-30T01:25:58.948Z" }, 190 | { url = "https://files.pythonhosted.org/packages/ef/4a/e2d0925e5ead474709eb89c6bbb9cd188396c9e3384a1f5d2491a38aeab6/lap-0.5.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1c34c3d8aefbf7d0cb709801ccf78c6ac31f4b1dc26c169ed1496ed3cb6f4556", size = 1728876, upload-time = "2024-11-30T01:26:25.744Z" }, 191 | { url = "https://files.pythonhosted.org/packages/46/89/73bad73b005e7f681f8cfa2c8748e9d766b91da781d07f300f86a9eb4f03/lap-0.5.12-cp312-cp312-win_amd64.whl", hash = "sha256:753ef9bd12805adbf0d09d916e6f0d271aebe3d2284a1f639bd3401329e436e5", size = 1476975, upload-time = "2024-11-30T01:26:40.341Z" }, 192 | { url = "https://files.pythonhosted.org/packages/d9/8d/00df0c44b728119fe770e0526f850b0a9201f23bf4276568aef5b372982e/lap-0.5.12-cp312-cp312-win_arm64.whl", hash = "sha256:83e507f6def40244da3e03c71f1b1f54ceab3978cde72a84b84caadd8728977e", size = 1466243, upload-time = "2024-11-30T01:26:43.202Z" }, 193 | { url = "https://files.pythonhosted.org/packages/e1/07/85a389eb4c6a9bf342f79811dd868ed3b6e56402f1dfa71474cec3c5ac30/lap-0.5.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c4fdbd8d94ad5da913ade49635bad3fc4352ee5621a9f785494c11df5412d6d", size = 1479752, upload-time = "2024-11-30T01:27:06.417Z" }, 194 | { url = "https://files.pythonhosted.org/packages/b1/01/46ba9ab4b9d95b43058591094e49ef21bd7e6fe2eb5202ece0b23240b2dc/lap-0.5.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2d01113eec42174e051ee5cebb5d33ec95d37bd2c422b7a3c09bbebaf30b635", size = 1477146, upload-time = "2024-11-30T01:27:26.769Z" }, 195 | { url = "https://files.pythonhosted.org/packages/7e/c3/9f6829a20e18c6ca3a3e97fcab815f0d888b552e3e37b892d908334d0f22/lap-0.5.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6e8ed53cb4d85fa0875092bc17436d7eeab2c7fb3574e551c611c352fea8c8", size = 1717458, upload-time = "2024-11-30T01:27:29.936Z" }, 196 | { url = "https://files.pythonhosted.org/packages/f9/bb/0f3a44d7220bd48f9a313a64f4c228a02cbb0fb1f55fd449de7a0659a5e2/lap-0.5.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dd54bf8bb48c87f6276555e8014d4ea27742d84ddbb0e7b68be575f4ca438d7", size = 1720277, upload-time = "2024-11-30T01:28:05.397Z" }, 197 | { url = "https://files.pythonhosted.org/packages/3e/48/5dcfd7f97a5ac696ad1fe750528784694c374ee64312bfbf96d14284f74a/lap-0.5.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9db0e048cfb561f21671a3603dc2761f108b3111da66a7b7d2f035974dcf966e", size = 1712562, upload-time = "2024-11-30T01:28:19.952Z" }, 198 | { url = "https://files.pythonhosted.org/packages/77/60/ac8702518e4d7c7a284b40b1aae7b4e264a029a8476cb674067a26c17f3c/lap-0.5.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:517b8bd02e56b8466244fc4c0988aece04e6f8b11f43406ae195b4ce308733fb", size = 1724195, upload-time = "2024-11-30T01:28:46.411Z" }, 199 | { url = "https://files.pythonhosted.org/packages/4c/3b/62181a81af89a6e7cefca2390d1f0822f7f6b73b40393ea04000c1ac0435/lap-0.5.12-cp313-cp313-win_amd64.whl", hash = "sha256:59dba008db14f640a20f4385916def4b343fa59efb4e82066df81db5a9444d5e", size = 1476213, upload-time = "2024-11-30T01:29:03.832Z" }, 200 | { url = "https://files.pythonhosted.org/packages/9f/4b/2db5ddb766cda2bdbf4012771d067d2b1c91e0e2d2c5ca0573efcd7ad321/lap-0.5.12-cp313-cp313-win_arm64.whl", hash = "sha256:30309f6aff8e4d616856ec8c6eec7ad5b48d2687887b931302b5c8e6dfac347a", size = 1465708, upload-time = "2024-11-30T01:29:34.141Z" }, 201 | ] 202 | 203 | [[package]] 204 | name = "mediafile" 205 | version = "0.13.0" 206 | source = { registry = "https://pypi.org/simple" } 207 | dependencies = [ 208 | { name = "filetype" }, 209 | { name = "mutagen" }, 210 | ] 211 | sdist = { url = "https://files.pythonhosted.org/packages/86/43/7726b251c591044b4ae2734049260e7a37c7e5b97a3fe1002443f9a3e829/mediafile-0.13.0.tar.gz", hash = "sha256:de71063e1bffe9733d6ccad526ea7dac8a9ce760105827f81ab0cb034c729a6d", size = 562194, upload-time = "2024-11-03T19:31:33.69Z" } 212 | wheels = [ 213 | { url = "https://files.pythonhosted.org/packages/9f/b0/363b4d1443a593398f9d3784f406385f075e8fd0991c35356e73fc37638a/mediafile-0.13.0-py3-none-any.whl", hash = "sha256:cd8d183d0e0671b5203a86e92cf4e3338ecc892a1ec9dcd7ec0ed87779e514cb", size = 21469, upload-time = "2024-11-03T19:31:17.46Z" }, 214 | ] 215 | 216 | [[package]] 217 | name = "musicbrainzngs" 218 | version = "0.7.1" 219 | source = { registry = "https://pypi.org/simple" } 220 | sdist = { url = "https://files.pythonhosted.org/packages/0a/67/3e74ae93d90ceeba72ed1a266dd3ca9abd625f315f0afd35f9b034acedd1/musicbrainzngs-0.7.1.tar.gz", hash = "sha256:ab1c0100fd0b305852e65f2ed4113c6de12e68afd55186987b8ed97e0f98e627", size = 117469, upload-time = "2020-01-11T17:38:47.581Z" } 221 | wheels = [ 222 | { url = "https://files.pythonhosted.org/packages/6d/fd/cef7b2580436910ccd2f8d3deec0f3c81743e15c0eb5b97dde3fbf33c0c8/musicbrainzngs-0.7.1-py2.py3-none-any.whl", hash = "sha256:e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10", size = 25289, upload-time = "2020-01-11T17:38:45.469Z" }, 223 | ] 224 | 225 | [[package]] 226 | name = "mutagen" 227 | version = "1.47.0" 228 | source = { registry = "https://pypi.org/simple" } 229 | sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186, upload-time = "2023-09-03T16:33:33.411Z" } 230 | wheels = [ 231 | { url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" }, 232 | ] 233 | 234 | [[package]] 235 | name = "numpy" 236 | version = "2.2.6" 237 | source = { registry = "https://pypi.org/simple" } 238 | resolution-markers = [ 239 | "python_full_version < '3.11'", 240 | ] 241 | sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } 242 | wheels = [ 243 | { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, 244 | { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, 245 | { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, 246 | { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, 247 | { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, 248 | { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, 249 | { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, 250 | { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, 251 | { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, 252 | { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, 253 | { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, 254 | { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, 255 | { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, 256 | { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, 257 | { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, 258 | { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, 259 | { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, 260 | { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, 261 | { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, 262 | { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, 263 | { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, 264 | { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, 265 | { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, 266 | { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, 267 | { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, 268 | { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, 269 | { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, 270 | { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, 271 | { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, 272 | { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, 273 | { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, 274 | { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, 275 | { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, 276 | { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, 277 | { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, 278 | { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, 279 | { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, 280 | { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, 281 | { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, 282 | { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, 283 | { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, 284 | { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, 285 | { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, 286 | { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, 287 | { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, 288 | { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, 289 | { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, 290 | { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, 291 | { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, 292 | { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, 293 | { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, 294 | { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, 295 | { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, 296 | { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, 297 | ] 298 | 299 | [[package]] 300 | name = "numpy" 301 | version = "2.3.4" 302 | source = { registry = "https://pypi.org/simple" } 303 | resolution-markers = [ 304 | "python_full_version >= '3.11'", 305 | ] 306 | sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } 307 | wheels = [ 308 | { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, 309 | { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, 310 | { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, 311 | { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, 312 | { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, 313 | { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, 314 | { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, 315 | { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, 316 | { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, 317 | { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, 318 | { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, 319 | { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, 320 | { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, 321 | { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, 322 | { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, 323 | { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, 324 | { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, 325 | { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, 326 | { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, 327 | { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, 328 | { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, 329 | { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, 330 | { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, 331 | { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, 332 | { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, 333 | { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, 334 | { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, 335 | { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, 336 | { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, 337 | { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, 338 | { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, 339 | { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, 340 | { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, 341 | { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, 342 | { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, 343 | { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, 344 | { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, 345 | { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, 346 | { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, 347 | { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, 348 | { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, 349 | { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, 350 | { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, 351 | { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, 352 | { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, 353 | { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, 354 | { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, 355 | { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, 356 | { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, 357 | { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, 358 | { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, 359 | { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, 360 | { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, 361 | { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, 362 | { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, 363 | { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, 364 | { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, 365 | { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, 366 | { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, 367 | { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, 368 | { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, 369 | { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, 370 | { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, 371 | { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, 372 | { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, 373 | { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, 374 | { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, 375 | { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, 376 | { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, 377 | { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, 378 | { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, 379 | { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, 380 | { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, 381 | ] 382 | 383 | [[package]] 384 | name = "packaging" 385 | version = "24.1" 386 | source = { registry = "https://pypi.org/simple" } 387 | sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788, upload-time = "2024-06-09T23:19:24.956Z" } 388 | wheels = [ 389 | { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985, upload-time = "2024-06-09T23:19:21.909Z" }, 390 | ] 391 | 392 | [[package]] 393 | name = "platformdirs" 394 | version = "4.5.0" 395 | source = { registry = "https://pypi.org/simple" } 396 | sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } 397 | wheels = [ 398 | { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, 399 | ] 400 | 401 | [[package]] 402 | name = "pluggy" 403 | version = "1.5.0" 404 | source = { registry = "https://pypi.org/simple" } 405 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } 406 | wheels = [ 407 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, 408 | ] 409 | 410 | [[package]] 411 | name = "pytest" 412 | version = "8.0.2" 413 | source = { registry = "https://pypi.org/simple" } 414 | dependencies = [ 415 | { name = "colorama", marker = "sys_platform == 'win32'" }, 416 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 417 | { name = "iniconfig" }, 418 | { name = "packaging" }, 419 | { name = "pluggy" }, 420 | { name = "tomli", marker = "python_full_version < '3.11'" }, 421 | ] 422 | sdist = { url = "https://files.pythonhosted.org/packages/3f/c0/238f25cb27495fdbaa5c48cef9886162e9df1f3d0e957fc8326d9c24fa2f/pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd", size = 1396924, upload-time = "2024-02-24T22:21:30.762Z" } 423 | wheels = [ 424 | { url = "https://files.pythonhosted.org/packages/a7/ea/d0ab9595a0d4b2320483e634123171deaf50885e29d442180efcbf2ed0b2/pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096", size = 333984, upload-time = "2024-02-24T22:21:27.561Z" }, 425 | ] 426 | 427 | [[package]] 428 | name = "pyyaml" 429 | version = "6.0.2" 430 | source = { registry = "https://pypi.org/simple" } 431 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } 432 | wheels = [ 433 | { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, 434 | { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, 435 | { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, 436 | { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, 437 | { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, 438 | { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, 439 | { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, 440 | { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, 441 | { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, 442 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, 443 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, 444 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, 445 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, 446 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, 447 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, 448 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, 449 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, 450 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, 451 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, 452 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, 453 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, 454 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, 455 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, 456 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, 457 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, 458 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, 459 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, 460 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, 461 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, 462 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, 463 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, 464 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, 465 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, 466 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, 467 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, 468 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, 469 | ] 470 | 471 | [[package]] 472 | name = "ruff" 473 | version = "0.14.2" 474 | source = { registry = "https://pypi.org/simple" } 475 | sdist = { url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96", size = 5539663, upload-time = "2025-10-23T19:37:00.956Z" } 476 | wheels = [ 477 | { url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1", size = 12533390, upload-time = "2025-10-23T19:36:18.044Z" }, 478 | { url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11", size = 12887187, upload-time = "2025-10-23T19:36:22.664Z" }, 479 | { url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3", size = 11925177, upload-time = "2025-10-23T19:36:24.778Z" }, 480 | { url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3", size = 12358285, upload-time = "2025-10-23T19:36:26.979Z" }, 481 | { url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8", size = 12303832, upload-time = "2025-10-23T19:36:29.192Z" }, 482 | { url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839", size = 13036995, upload-time = "2025-10-23T19:36:31.861Z" }, 483 | { url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7", size = 14512649, upload-time = "2025-10-23T19:36:33.915Z" }, 484 | { url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc", size = 14088182, upload-time = "2025-10-23T19:36:36.983Z" }, 485 | { url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a", size = 13599516, upload-time = "2025-10-23T19:36:39.208Z" }, 486 | { url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096", size = 13272690, upload-time = "2025-10-23T19:36:41.453Z" }, 487 | { url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df", size = 13496497, upload-time = "2025-10-23T19:36:43.61Z" }, 488 | { url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05", size = 12266116, upload-time = "2025-10-23T19:36:45.625Z" }, 489 | { url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5", size = 12281345, upload-time = "2025-10-23T19:36:47.618Z" }, 490 | { url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e", size = 12629296, upload-time = "2025-10-23T19:36:49.789Z" }, 491 | { url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770", size = 13371610, upload-time = "2025-10-23T19:36:51.882Z" }, 492 | { url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9", size = 12415318, upload-time = "2025-10-23T19:36:53.961Z" }, 493 | { url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279, upload-time = "2025-10-23T19:36:56.578Z" }, 494 | { url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" }, 495 | ] 496 | 497 | [[package]] 498 | name = "tomli" 499 | version = "2.0.1" 500 | source = { registry = "https://pypi.org/simple" } 501 | sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } 502 | wheels = [ 503 | { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, 504 | ] 505 | 506 | [[package]] 507 | name = "typing-extensions" 508 | version = "4.12.2" 509 | source = { registry = "https://pypi.org/simple" } 510 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" } 511 | wheels = [ 512 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" }, 513 | ] 514 | 515 | [[package]] 516 | name = "unidecode" 517 | version = "1.3.8" 518 | source = { registry = "https://pypi.org/simple" } 519 | sdist = { url = "https://files.pythonhosted.org/packages/f7/89/19151076a006b9ac0dd37b1354e031f5297891ee507eb624755e58e10d3e/Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4", size = 192701, upload-time = "2024-01-11T11:58:35.609Z" } 520 | wheels = [ 521 | { url = "https://files.pythonhosted.org/packages/84/b7/6ec57841fb67c98f52fc8e4a2d96df60059637cba077edc569a302a8ffc7/Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39", size = 235494, upload-time = "2024-01-11T11:58:33.012Z" }, 522 | ] 523 | --------------------------------------------------------------------------------