diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2eb42db..0cca5dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - python: ["3.7", "3.8", "3.9", "3.10"] + python: ["3.8", "3.9", "3.10", "3.11", "3.12"] beets: ["1.4.9", "1.5.0", "1.6.0"] steps: - uses: actions/checkout@v3 @@ -36,17 +36,34 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: python${{ matrix.python }}_beets${{ matrix.beets }} COVERALLS_PARALLEL: true - - name: Flake8 + - name: Lint flake8 run: flake8 . --output-file flake.log --exit-zero - - name: Mypy - run: mypy --strict >> flake.log || true - - name: Pylint + - name: Lint mypy + run: mypy > mypy.log || true + - name: Lint pylint run: pylint --output pylint.log --exit-zero $(git ls-files '*.py') + - name: Set project version + run: echo PROJECT_VERSION="$(git describe --tags | sed 's/-[^-]*$//')" >> $GITHUB_ENV - name: SonarCloud Scan if: ${{ matrix.beets == '1.5.0' && matrix.python == '3.8' }} uses: SonarSource/sonarcloud-github-action@master with: - args: -Dsonar.branch.name=${{ github.ref_name }} + args: > + -Dsonar.branch.name=${{ github.ref_name }} + -Dsonar.organization=snejus + -Dsonar.projectKey=snejus_beetcamp + -Dsonar.projectVersion=${{ env.PROJECT_VERSION }} + -Dsonar.coverage.exclusions=tests/* + -Dsonar.exclusions=tests/* + -Dsonar.python.coverage.reportPaths=.reports/coverage.xml + -Dsonar.python.flake8.reportPaths=flake.log + -Dsonar.python.pylint.reportPaths=pylint.log + -Dsonar.python.mypy.reportPaths=mypy.log + -Dsonar.python.version=3.8 + -Dsonar.python.xunit.reportPath=.reports/test-report.xml + -Dsonar.sources=beetsplug/bandcamp + -Dsonar.tests=tests + -Dsonar.test.inclusions=tests/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index fc602f1..574eada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,51 @@ +## Unreleased + + +## [0.18.0] 2024-04-28 + +### Removed + +- Dropped support for `python 3.7`. + +### Fixed + +- (#52) genre: do not fail parsing a release without any keywords, for example + https://amniote-editions.bandcamp.com/album/ae-mj-0011-the-collective-capsule-vol-1 +- (#54) Ensure that our genre matching rules also apply to genres delimited by a dash, not + only space. + +### Updated + +- `album`: + - handle some edge cases when string **EP** or **LP** is followed with data relevant to + the album + - do not remove artist or label when it is preceded by **` x `** or followed by characters + **`'`**, **`_`** and **`&`**, or words **EP**, **LP** and **deluxe** + - handle apostrophes more reliably + - Do not remove **VA** or **V/A** from the beginning when followed by a word or a number + +- `album` / `title`: + - Remove **`Various -`** from album and track names + - Handle this [album sent to us by the devil himself] + +- `catalognum`: + - allow catalogue numbers like **Dystopian LP01** + - parse a _range_ of catalogue numbers when it is present, for example + **TFT013SR - TFT-016SR** + +- `comments`: use value `None` when there are no comments. In contrast to returning an + empty string, this way during beets import the previous comment on the track will be + kept if the Bandcamp release does not have a description. + +- `label`: label is now correctly obtained for single releases when it is available. + +- `title`: + - consider **with** and **w/** as markers for collaborating artists + - remove **`bonus -`** + - `Artist - Title (bonus - something)` -> **`Artist - Title (something)`** + +[album sent to us by the devil himself]: https://examine-archive.bandcamp.com/album/va-examine-archive-international-sampler-xmn01 + ## [0.17.2] 2023-08-09 ### Fixed diff --git a/beetsplug/bandcamp/__init__.py b/beetsplug/bandcamp/__init__.py index 31edc94..7b3f710 100644 --- a/beetsplug/bandcamp/__init__.py +++ b/beetsplug/bandcamp/__init__.py @@ -15,25 +15,31 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Adds bandcamp album search support to the autotagger.""" -from __future__ import absolute_import, division, print_function, unicode_literals + +from __future__ import annotations import logging import re +from contextlib import contextmanager from functools import lru_cache, partial from html import unescape -from operator import itemgetter, truth -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Sequence, Union +from itertools import chain +from operator import itemgetter +from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Literal, Sequence import requests from beets import IncludeLazyConfig, __version__, config, library, plugins -from beets.autotag.hooks import AlbumInfo, TrackInfo from beetsplug import fetchart # type: ignore[attr-defined] -from ._metaguru import Metaguru -from ._search import search_bandcamp +from .metaguru import Metaguru +from .search import search_bandcamp + +if TYPE_CHECKING: + from beets.autotag.hooks import AlbumInfo, TrackInfo JSONDict = Dict[str, Any] +CandidateType = Literal["album", "track"] DEFAULT_CONFIG: JSONDict = { "include_digital_only_tracks": True, @@ -50,6 +56,7 @@ } ALBUM_URL_IN_TRACK = re.compile(r' str: return "" return unescape(response.text) - def guru(self, url, attr): - # type: (str, str) -> Optional[Union[TrackInfo, List[AlbumInfo]]] + def guru(self, url: str) -> Metaguru: + return Metaguru.from_html(self._get(url), config=self.config.flatten()) + + @contextmanager + def handle_error(self, url: str) -> Iterator[Any]: """Return Metaguru for the given URL.""" - config = self.config.flatten() if hasattr(self, "config") else DEFAULT_CONFIG try: - return getattr(Metaguru.from_html(self._get(url), config=config), attr) + yield except (KeyError, ValueError, AttributeError, IndexError): - self._info("Failed obtaining {} from {}", attr, url) + self._info("Failed obtaining {}", url) except Exception: # pylint: disable=broad-except i_url = "https://github.com/snejus/beetcamp/issues/new" self._exc("Unexpected error obtaining {}, please report at {}", url, i_url) - return None def _from_bandcamp(clue: str) -> bool: """Check if the clue is likely to be a bandcamp url. + We could check whether 'bandcamp' is found in the url, however, we would be ignoring cases where the publisher uses their own domain (for example https://eaux.ro) which in reality points to their Bandcamp page. Historically, we found that regardless @@ -108,18 +117,24 @@ def _from_bandcamp(clue: str) -> bool: class BandcampAlbumArt(BandcampRequestsHandler, fetchart.RemoteArtSource): NAME = "Bandcamp" - def get(self, album, plugin, paths): - # type: (AlbumInfo, plugins.BeetsPlugin, List[str]) -> Iterable[fetchart.Candidate] # noqa + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.config = self._config + + def get(self, album: AlbumInfo, *_: Any) -> Iterable[fetchart.Candidate]: """Return the url for the cover from the bandcamp album page. + This only returns cover art urls for bandcamp albums (by id). """ url = album.mb_albumid if not _from_bandcamp(url): self._info("Not fetching art for a non-bandcamp album URL") else: - image = self.guru(url, "image") - if image: - yield self._candidate(url=image, match=fetchart.Candidate.MATCH_EXACT) + with self.handle_error(url): + if image := self.guru(url).image: + yield self._candidate( + url=image, match=fetchart.Candidate.MATCH_EXACT + ) def urlify(pretty_string: str) -> str: @@ -154,8 +169,12 @@ def loaded(self) -> None: plugin.sources = [bandcamp_fetchart, *plugin.sources] break - def _find_url(self, item: library.Item, name: str, _type: str) -> str: - """If the item has previously been imported, `mb_albumid` (or `mb_trackid` + def _find_url_in_item( + self, item: library.Item, name: str, _type: CandidateType + ) -> str: + """Try to extract release URL from the library item. + + If the item has previously been imported, `mb_albumid` (or `mb_trackid` for singletons) contains the release url. As of 2022 April, Bandcamp purchases (at least in FLAC format) contain string @@ -175,24 +194,23 @@ def _find_url(self, item: library.Item, name: str, _type: str) -> str: self._info("Fetching the URL attached to the first item, {}", url) return url - m = re.match(r"Visit (https:[\w/.-]+com)", item.comments) - urlified_name = urlify(name) - if m and urlified_name: - label = m.expand(r"\1") - url = "/".join([label, _type, urlified_name]) + if (m := LABEL_URL_IN_COMMENT.match(item.comments)) and ( + urlified_name := urlify(name) + ): + label = m.group(1) + url = f"{label}/{_type}/{urlified_name}" self._info("Trying our guess {} before searching", url) return url return "" - def candidates(self, items, artist, album, va_likely, extra_tags=None): - # type: (List[library.Item], str, str, bool, Any) -> Iterable[AlbumInfo] - """Return a sequence of AlbumInfo objects that match the - album whose items are provided or are being searched. - """ + def candidates( + self, items: List[library.Item], artist: str, album: str, *_: Any, **__: Any + ) -> Iterable[AlbumInfo]: + """Return a sequence of album candidates matching given artist and album.""" label = "" if items and album == items[0].album and artist == items[0].albumartist: label = items[0].label - url = self._find_url(items[0], album, "album") + url = self._find_url_in_item(items[0], album, "album") if url: initial_guess = self.get_album_info(url) if initial_guess: @@ -204,17 +222,13 @@ def candidates(self, items, artist, album, va_likely, extra_tags=None): search = {"query": album, "artist": artist, "label": label, "search_type": "a"} results = map(itemgetter("url"), self._search(search)) - for res in filter(truth, map(self.get_album_info, results)): - yield from res or [None] - - def item_candidates(self, item, artist, title): - # type: (library.Item, str, str) -> Iterable[TrackInfo] - """Return a sequence of TrackInfo objects that match the provided item. - If the track was downloaded directly from bandcamp, it should contain - a comment saying 'Visit ' - we look at this first by converting - title into the format that Bandcamp use. - """ - url = self._find_url(item, title, "track") + yield from chain.from_iterable(filter(None, map(self.get_album_info, results))) + + def item_candidates( + self, item: library.Item, artist: str, title: str + ) -> Iterable[TrackInfo]: + """Return a sequence of singleton candidates matching given artist and title.""" + url = self._find_url_in_item(item, title, "track") label = "" if item and title == item.title and artist == item.artist: label = item.label @@ -225,9 +239,9 @@ def item_candidates(self, item, artist, title): search = {"query": title, "artist": artist, "label": label, "search_type": "t"} results = map(itemgetter("url"), self._search(search)) - yield from filter(truth, map(self.get_track_info, results)) + yield from filter(None, map(self.get_track_info, results)) - def album_for_id(self, album_id: str) -> Optional[AlbumInfo]: + def album_for_id(self, album_id: str) -> AlbumInfo | None: """Fetch an album by its bandcamp ID.""" if not _from_bandcamp(album_id): self._info("Not a bandcamp URL, skipping") @@ -244,7 +258,7 @@ def album_for_id(self, album_id: str) -> Optional[AlbumInfo]: albums = sorted(albums, key=lambda x: pref_to_idx.get(x.media, 100)) return albums[0] - def track_for_id(self, track_id: str) -> Optional[TrackInfo]: + def track_for_id(self, track_id: str) -> TrackInfo | None: """Fetch a track by its bandcamp ID.""" if _from_bandcamp(track_id): return self.get_track_info(track_id) @@ -252,8 +266,9 @@ def track_for_id(self, track_id: str) -> Optional[TrackInfo]: self._info("Not a bandcamp URL, skipping") return None - def get_album_info(self, url: str) -> Optional[List[AlbumInfo]]: + def get_album_info(self, url: str) -> List[AlbumInfo] | None: """Return an AlbumInfo object for a bandcamp album page. + If track url is given by mistake, find and fetch the album url instead. """ html = self._get(url) @@ -261,11 +276,14 @@ def get_album_info(self, url: str) -> Optional[List[AlbumInfo]]: m = ALBUM_URL_IN_TRACK.search(html) if m: url = re.sub(r"/track/.*", m.expand(r"\1"), url) - return self.guru(url, "albums") - def get_track_info(self, url: str) -> Optional[TrackInfo]: - """Returns a TrackInfo object for a bandcamp track page.""" - return self.guru(url, "singleton") + with self.handle_error(url): + return self.guru(url).albums + + def get_track_info(self, url: str) -> TrackInfo | None: + """Return a TrackInfo object for a bandcamp track page.""" + with self.handle_error(url): + return self.guru(url).singleton def _search(self, data: JSONDict) -> Iterable[JSONDict]: """Return a list of track/album URLs of type search_type matching the query.""" @@ -275,7 +293,7 @@ def _search(self, data: JSONDict) -> Iterable[JSONDict]: return results[: self.config["search_max"].as_number()] -def get_args(args: List[str]) -> Any: +def get_args() -> Any: from argparse import Action, ArgumentParser if TYPE_CHECKING: @@ -292,8 +310,13 @@ def get_args(args: List[str]) -> Any: ) class UrlOrQueryAction(Action): - def __call__(self, parser, namespace, values, option_string=None): - # type: (Any, Namespace, Any, Any) -> None + def __call__( + self, + parser: ArgumentParser, + namespace: Namespace, + values: Any, + option_string: str | None = None, + ) -> None: if values: if values.startswith("https://"): target = "release_url" @@ -302,7 +325,7 @@ def __call__(self, parser, namespace, values, option_string=None): del namespace.release_url setattr(namespace, target, values) - exclusive = parser.add_mutually_exclusive_group() + exclusive = parser.add_mutually_exclusive_group(required=True) exclusive.add_argument( "release_url", action=UrlOrQueryAction, @@ -327,17 +350,14 @@ def __call__(self, parser, namespace, values, option_string=None): type=int, help="Open search result indexed by INDEX in the browser", ) - if not args: - parser.print_help() - parser.exit() - return parser.parse_args(args=args) + + return parser.parse_args() def main() -> None: import json - import sys - args = get_args(sys.argv[1:]) + args = get_args() search_vars = vars(args) index = search_vars.pop("index", None) diff --git a/beetsplug/bandcamp/_tracks.py b/beetsplug/bandcamp/_tracks.py deleted file mode 100644 index 622220e..0000000 --- a/beetsplug/bandcamp/_tracks.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Module with tracks parsing functionality.""" -import itertools as it -import operator as op -import re -import sys -from collections import Counter -from dataclasses import dataclass -from functools import reduce -from typing import Iterator, List, Optional, Set - -from ordered_set import OrderedSet - -from ._helpers import CATNUM_PAT, Helpers, JSONDict, _remix_pat -from .track import Track - -if sys.version_info.minor > 7: - from functools import cached_property # pylint: disable=ungrouped-imports -else: - from cached_property import cached_property # type: ignore # pylint: disable=import-error # noqa - -digiwords = r""" - # must contain at least one of - (\W*(bandcamp|digi(tal)?|exclusive|bonus|bns|unreleased))+ - # and may be followed by - (\W(track|only|tune))* - """ -DIGI_ONLY_PATTERN = re.compile( - rf""" -\s* # all preceding space -( - (^{digiwords}[.:\d\s]+\s) # begins with 'Bonus.', 'Bonus 1.' or 'Bonus :' - | [\[(]{digiwords}[\])]\W* # delimited by brackets, '[Bonus]', '(Bonus) -' - | [*]{digiwords}[*] # delimited by asterisks, '*Bonus*' - | ([ ]{digiwords}$) # might not be delimited if at the end, '... Bonus' -) -\s* # all succeeding space - """, - re.I | re.VERBOSE, -) -DELIMITER_PAT = re.compile(r" ([^\w&()+/[\] ]) ") -ELP_ALBUM_PAT = re.compile(r"[- ]*\[([^\]]+ [EL]P)\]+") # Title [Some Album EP] -TITLE_IN_QUOTES = re.compile(r'^(.+[^ -])[ -]+"([^"]+)"$') -NUMBER_PREFIX = re.compile(r"(^|- )\d{2,}\W* ") - - -@dataclass -class Tracks(List[Track]): - tracks: List[Track] - - def __iter__(self) -> Iterator[Track]: - return iter(self.tracks) - - def __len__(self) -> int: - return len(self.tracks) - - @classmethod - def from_json(cls, meta: JSONDict) -> "Tracks": - try: - tracks = [{**t, **t["item"]} for t in meta["track"]["itemListElement"]] - except (TypeError, KeyError): - tracks = [meta] - - names = [i.get("name", "") for i in tracks] - names = cls.split_quoted_titles(names) - names = cls.remove_number_prefix(names) - delim = cls.track_delimiter(names) - for track, name in zip(tracks, names): - track["name_parts"] = {"clean": name} - track["delim"] = delim - tracks = cls.common_name_parts(tracks, names) - return cls([Track.from_json(t, delim, Helpers.get_label(meta)) for t in tracks]) - - @cached_property - def first(self) -> Track: - return self.tracks[0] - - @staticmethod - def split_quoted_titles(names: List[str]) -> List[str]: - if len(names) > 1 and all(TITLE_IN_QUOTES.match(n) for n in names): - return [TITLE_IN_QUOTES.sub(r"\1 - \2", n) for n in names] - return names - - @staticmethod - def remove_number_prefix(names: List[str]) -> List[str]: - if len(names) > 1 and all(NUMBER_PREFIX.search(n) for n in names): - return [NUMBER_PREFIX.sub(r"\1", n) for n in names] - return names - - @staticmethod - def common_name_parts(tracks, names): - # type: (List[JSONDict], List[str]) -> List[JSONDict] - """Parse track names for parts that require knowledge of the other names. - - 1. Split each track name into words - 2. Find the list of words that are common to all tracks - a. check the *first* and the *last* word for the catalog number - 1. If found, remove it from every track name - b. check whether tracks start the same way. This indicates an album with - one unique root title and a couple of its remixes. This is especially - relevant when the remix is not delimited appropriately. - Return the catalog number and the new list of names. - """ - names_tokens = map(str.split, names) - common_words = reduce(op.and_, [OrderedSet(x) for x in names_tokens]) - if not common_words: - return tracks - - matches = (CATNUM_PAT["anywhere"].search(common_words[i]) for i in [0, -1]) - try: - cat, word = next(((m.group(1), m.string) for m in matches if m)) - except StopIteration: - pass - else: - for track in tracks: - track["name_parts"].update( - catalognum=cat, - clean=track["name_parts"]["clean"].replace(word, "").strip(), - ) - - joined = " ".join(common_words) - if joined in names: # it is one of the track names (root title) - for track in tracks: - leftover = track["name_parts"]["clean"].replace(joined, "").lstrip() - # looking for a remix without brackets - if re.fullmatch(_remix_pat, leftover, re.I): - track["name_parts"]["clean"] = f"{joined} ({leftover})" - - return tracks - - @cached_property - def raw_names(self) -> List[str]: - return [j.name for j in self.tracks] - - @cached_property - def original_artists(self) -> List[str]: - """Return all unique unsplit (original) main track artists.""" - return list(dict.fromkeys(j.artist for j in self.tracks)) - - @property - def artists(self) -> List[str]: - """Return all unique split main track artists. - - "Artist1 x Artist2" -> ["Artist1", "Artist2"] - """ - return list(dict.fromkeys(it.chain(*(j.artists for j in self.tracks)))) - - @property - def remixers(self) -> List[str]: - """Return all remix artists.""" - return [ - t.remix.remixer - for t in self.tracks - if t.remix and not t.remix.by_other_artist - ] - - @property - def other_artists(self) -> Set[str]: - """Return all unique remix and featuring artists.""" - ft = [j.ft for j in self.tracks if j.ft] - return set(it.chain(self.remixers, ft)) - - @cached_property - def all_artists(self) -> Set[str]: - """Return all unique (1) track, (2) remix, (3) featuring artists.""" - return self.other_artists | set(self.original_artists) - - @cached_property - def artistitles(self) -> str: - """Returned artists and titles joined into one long string.""" - return " ".join(it.chain(self.raw_names, self.all_artists)).lower() - - @cached_property - def single_catalognum(self) -> Optional[str]: - """Return catalognum if every track contains the same one.""" - cats = [t.catalognum for t in self if t.catalognum] - if len(cats) == len(self) and len(set(cats)) == 1: - return cats[0] - return None - - @cached_property - def albums_in_titles(self) -> Set[str]: - return {t.album for t in self if t.album} - - def adjust_artists(self, albumartist: str) -> None: - """Handle some track artist edge cases. - - These checks require knowledge of the entire release, therefore cannot be - performed within the context of a single track. - - * When artist name is mistaken for the track_alt - * When artist and title are delimited by '-' without spaces - * When artist and title are delimited by a UTF-8 dash equivalent - * Defaulting to the album artist - """ - track_alts = {t.track_alt for t in self.tracks if t.track_alt} - artists = [t.artist for t in self.tracks if t.artist] - - for t in [track for track in self.tracks if not track.artist]: - if t.track_alt and len(track_alts) == 1: # only one track_alt - # the only track that parsed a track alt - it's most likely a mistake - # one artist was confused for a track alt, like 'B2', - reverse this - t.artist, t.track_alt = t.track_alt, None - elif len(artists) == len(self) - 1: # only 1 missing artist - # if this is a remix and the parsed title is part of the albumartist or - # is one of the track artists, we made a mistake parsing the remix: - # it is most probably the edge case where the `main_title` is a - # legitimate artist and the track title is something like 'Hello Remix' - if t.remix and (t.main_title in albumartist): - t.artist, t.title = t.main_title, t.remix.remix - # this is the only artist that didn't get parsed - relax the rule - # and try splitting with '-' without spaces - split = t.title.split("-") - if len(split) == 1: - # attempt to split by another ' ? ' where '?' may be some utf-8 - # alternative of a dash - split = [s for s in DELIMITER_PAT.split(t.title) if len(s) > 1] - if len(split) > 1: - t.artist, t.title = split - if not t.artist: - # default to the albumartist - t.artist = albumartist - - @staticmethod - def track_delimiter(names: List[str]) -> str: - """Return the track parts delimiter that is in effect in the current release. - In some (rare) situations track parts are delimited by a pipe character - or some UTF-8 equivalent of a dash. - - This checks every track for the first character (see the regex for exclusions) - that splits it. The character that splits the most and at least half of - the tracks is the character we need. - - If no such character is found, or if we have just one track, return a dash '-'. - """ - - def get_delim(string: str) -> str: - m = DELIMITER_PAT.search(string) - return m.group(1) if m else "-" - - delim, count = Counter(map(get_delim, names)).most_common(1).pop() - return delim if (len(names) == 1 or count > len(names) / 2) else "-" diff --git a/beetsplug/bandcamp/album.py b/beetsplug/bandcamp/album.py index d738437..702062a 100644 --- a/beetsplug/bandcamp/album.py +++ b/beetsplug/bandcamp/album.py @@ -1,15 +1,11 @@ """Module with album parsing logic.""" -import sys -from dataclasses import dataclass -import re -from typing import List, Set, Dict, Any -from ._helpers import PATTERNS, Helpers +import re +from dataclasses import dataclass +from functools import cached_property +from typing import Any, Dict, List, Optional -if sys.version_info.minor > 7: - from functools import cached_property # pylint: disable=ungrouped-imports -else: - from cached_property import cached_property # type: ignore # pylint: disable=import-error # noqa +from .helpers import PATTERNS, Helpers JSONDict = Dict[str, Any] @@ -19,69 +15,80 @@ class AlbumName: _series = r"(?i:\b(part|volume|pt|vol)\b\.?)" SERIES = re.compile(rf"{_series}[ ]?[A-Z\d.-]+\b") SERIES_FMT = re.compile(rf"^(.+){_series} *0*") - INCL = re.compile(r"[^][\w]*inc[^()]+mix(es)?[^()-]*\W?", re.I) - EPLP = re.compile(r"\S*(?:Double )?(\b[EL]P\b)\S*", re.I) - - meta: JSONDict + REMIX_IN_TITLE = re.compile(r"[\( :]+(with re|inc|\+).*mix(\)|(.*$))", re.I) + CLEAN_EPLP = re.compile(r"(?:[([]|Double ){0,2}(\b[EL]P\b)\S?", re.I) + EPLP_ALBUM = re.compile( + r"\b((?:(?!VA|Various|-)[^: ]+ )+)([EL]P(?! *\d)(?: [\w#][^ ]+$)?)" + ) + QUOTED_ALBUM = re.compile(r"(['\"])([^'\"]+)\1( VA\d+)*( |$)") + ALBUM_IN_DESC = re.compile(r"(?:Title: ?|Album(?::|/Single) )([^\n]+)") + CLEAN_VA_EXCLUDE = re.compile(r"\w various artists \w", re.I) + CLEAN_VA = re.compile( + r""" + (?<=^)v/?a\b(?!\ \w)[^A-z(]* + | \W*Various\ Artists?\b(?!\ [A-z])[^A-z(]* + """, + re.IGNORECASE + re.VERBOSE, + ) + COMPILATION_IN_TITLE = re.compile(r"compilation|best of|anniversary", re.I) + + original: str description: str - albums_in_titles: Set[str] + from_track_titles: Optional[str] remove_artists = True @cached_property - def in_description(self) -> str: - """Check description for the album name header and return whatever follows it - if found. - """ - m = re.search(r"(Title: ?|Album(:|/Single) )([^\n]+)", self.description) - if m: + def from_description(self) -> Optional[str]: + """Try finding album name in the release description.""" + if m := self.ALBUM_IN_DESC.search(self.description): self.remove_artists = False - return m.group(3).strip() - return "" + return m.group(1).strip() - @cached_property - def original(self) -> str: - return self.meta.get("name") or "" + return None @cached_property def mentions_compilation(self) -> bool: - return bool(re.search(r"compilation|best of|anniversary", self.original, re.I)) + return bool(self.COMPILATION_IN_TITLE.search(self.original)) @cached_property - def parsed(self) -> str: - """ - Search for the album name in the following order and return the first match: - 1. Album name is found in *all* track names - 2. When 'EP' or 'LP' is in the release name, album name is what precedes it. - 3. If some words are enclosed in quotes in the release name, it is assumed - to be the album name. Remove the quotes in such case. + def from_title(self) -> Optional[str]: + """Try to guess album name from the original title. + + Return the first match from below, defaulting to None: + 1. If 'EP' or 'LP' is in the original name, album name is what precedes it. + 2. If quotes are used in the title, they probably contain the album name. """ - if len(self.albums_in_titles) == 1: - return next(iter(self.albums_in_titles)) - - album = self.original - for pat in [ - r"(((&|#?\b(?!Double|VA|Various)(\w|[^\w| -])+) )+[EL]P)", - r"((['\"])([^'\"]+)\2( VA\d+)*)( |$)", - ]: - m = re.search(pat, album) - if m: - album = m.group(1).strip() - return re.sub(r"^['\"](.+)['\"]$", r"\1", album) - return album + if m := self.EPLP_ALBUM.search(self.original): + return " ".join(i.strip(" '") for i in m.groups()) + + if m := self.QUOTED_ALBUM.search(self.original): + return m.expand(r"\2\3") + + return None @cached_property - def album_sources(self) -> List[str]: - return list(filter(None, [self.in_description, self.parsed, self.original])) + def album_names(self) -> List[str]: + priority_list = [ + self.from_track_titles, + self.from_description, + self.from_title, + self.original, + ] + return list(filter(None, priority_list)) @cached_property def name(self) -> str: - return self.in_description or self.parsed or self.original + return next(iter(self.album_names)) @cached_property - def series(self) -> str: - m = self.SERIES.search("\n".join(self.album_sources)) - return m.group() if m else "" + def series_part(self) -> Optional[str]: + """Return series if it is found in any of the album names.""" + for name in self.album_names: + if m := self.SERIES.search(name): + return m.group() + + return None @staticmethod def format_series(m: re.Match) -> str: # type: ignore[type-arg] @@ -100,7 +107,7 @@ def format_series(m: re.Match) -> str: # type: ignore[type-arg] def standardize_series(self, album: str) -> str: """Standardize 'Vol', 'Part' etc. format.""" - series = self.series + series = self.series_part if not series: return album @@ -129,37 +136,52 @@ def remove_label(name: str, label: str) -> str: \W* # pick up any punctuation (? str: + if not cls.CLEAN_VA_EXCLUDE.search(name): + return cls.CLEAN_VA.sub(" ", name) + + return name + @classmethod def clean(cls, name: str, to_clean: List[str], label: str = "") -> str: """Return clean album name. Catalogue number and artists to be removed are provided as 'to_clean'. """ - name = PATTERNS["ft"].sub(" ", name) name = re.sub(r"^\[(.*)\]$", r"\1", name) - escaped = [re.escape(x) for x in filter(None, to_clean)] + [ - r"Various Artists?\b(?! [A-z])( \d+)?" - ] - for arg in escaped: - name = re.sub(rf" *(?i:(compiled )?by|vs|\W*split w) {arg}", "", name) - if not re.search(rf"\w {arg} \w|of {arg}", name, re.I): + for w in map(re.escape, filter(None, to_clean)): + name = re.sub(rf" *(?i:(compiled )?by|vs|\W*split w) {w}", "", name) + if not re.search( + rf"\w {w} \w|(of|&) {w}|{w}(['_\d]| (deluxe|[el]p\b|&))", name, re.I + ): name = re.sub( - rf"(^|[^'\])\w]|_|\b)+(?i:{arg})([^'(\[\w]|_|(\d+$))*", " ", name + rf""" + (? str: @@ -173,7 +195,7 @@ def check_eplp(self, album: str) -> str: else: look_for = r"((?!The|This)\b[A-Z][^ \n]+\b )+" - m = re.search(rf"{look_for}[EL]P", self.description) + m = re.search(rf"{look_for}[EL]P\b", self.description) return m.group() if m else album def get( @@ -183,18 +205,19 @@ def get( artists: List[str], label: str, ) -> str: - album = self.name + original_album = self.name to_clean = [catalognum] if self.remove_artists: to_clean.extend(original_artists + artists) + to_clean = sorted(filter(None, set(to_clean)), key=len, reverse=True) - album = self.clean(album, sorted(to_clean, key=len, reverse=True), label) + album = self.clean(original_album, to_clean, label) if album.startswith("("): - album = self.name + album = original_album album = self.check_eplp(self.standardize_series(album)) if "split ep" in album.lower() or (not album and len(artists) == 2): album = " / ".join(artists) - return album or catalognum or self.name + return album or catalognum or original_album diff --git a/beetsplug/bandcamp/_helpers.py b/beetsplug/bandcamp/helpers.py similarity index 73% rename from beetsplug/bandcamp/_helpers.py rename to beetsplug/bandcamp/helpers.py index ef54543..97ea90f 100644 --- a/beetsplug/bandcamp/_helpers.py +++ b/beetsplug/bandcamp/helpers.py @@ -1,8 +1,9 @@ """Module with a Helpers class that contains various static, independent functions.""" -import itertools as it -import operator as op + import re from functools import lru_cache, partial +from itertools import chain, starmap +from operator import contains from typing import Any, Dict, Iterable, List, NamedTuple, Pattern from beets.autotag.hooks import AlbumInfo @@ -30,7 +31,7 @@ class MediaInfo(NamedTuple): CATALOGNUM_CONSTRAINT = r"""(?[\[(])|\b) # bracket or word boundary - (ft|feat|featuring)[. ] # one of the three ft variations - ( - # when it does not start with a bracket, do not allow " - " in it, otherwise - # we may match full track name - (?(br)|(?!.*[ ]-[ ].*)) - # anything but brackets or a slash, except for a slash preceded - # by a non-space (can be part of artist or title) - (?:[^]\[()/]|\S/)+ + [ ]* # all preceding space + ((?P
[([{])|\b) # bracket or word boundary + (?P + (ft|feat|featuring|(?<=\()with|w/(?![ ]you))[. ]+ # any ft variation + (?P.+?) + (?((?P[^])]+) )?\b((re)?mix|edit|bootleg)\b[^])]*)" +REMIX = re.compile( + r"(?P((?P[^])]+) )?\b((re)?mix|edit|bootleg)\b[^])]*)", re.I +) +CAMELCASE = re.compile(r"(?<=[a-z])(?=[A-Z])") + + +def split_artist_title(m: re.Match) -> str: + """See for yourself. + + https://examine-archive.bandcamp.com/album/va-examine-archive-international-sampler-xmn01 + """ + artist, title = m.groups() + artist = CAMELCASE.sub(" ", artist) + title = CAMELCASE.sub(" ", title) + + return f"{artist} - {title}" + + # fmt: off CLEAN_PATTERNS = [ - (re.compile(rf"(([\[(])|(^| ))\*?({'|'.join(rm_strings)})(?(2)[])]|( |$))", re.I), ""), # noqa + (re.compile(rf"(([\[(])|(^| ))\*?({'|'.join(rm_strings)})(?(2)[])]|([- ]|$))", re.I), ""), # noqa (re.compile(r" -(\S)"), r" - \1"), # hi -bye -> hi - bye (re.compile(r"(\S)- "), r"\1 - "), # hi- bye -> hi - bye (re.compile(r" +"), " "), # hi bye -> hi bye (re.compile(r"(- )?\( *"), "("), # hi - ( bye) -> hi (bye) (re.compile(r" \)+|(\)+$)"), ")"), # hi (bye )) -> hi (bye) (re.compile(r"- Reworked"), "(Reworked)"), # bye - Reworked -> bye (Reworked) # noqa - (re.compile(rf"(\({_remix_pat})$", re.I), r"\1)"), # bye - (Some Mix -> bye - (Some Mix) # noqa - (re.compile(rf"- *({_remix_pat})$", re.I), r"(\1)"), # bye - Some Mix -> bye (Some Mix) # noqa + (re.compile(rf"(\({REMIX.pattern})$", re.I), r"\1)"), # bye - (Some Mix -> bye - (Some Mix) # noqa + (re.compile(rf"- *({REMIX.pattern})$", re.I), r"(\1)"), # bye - Some Mix -> bye (Some Mix) # noqa (re.compile(r'(^|- )[“"]([^”"]+)[”"]( \(|$)'), r"\1\2\3"), # "bye" -> bye; hi - "bye" -> hi - bye # noqa - (re.compile(r"\((?i:(the )?(remixes))\)"), r"\2"), # Album (Remixes) -> Album Remixes # noqa + (re.compile(r"\((the )?(remixes)\)", re.I), r"\2"), # Album (Remixes) -> Album Remixes # noqa + (re.compile(r"examine-.+CD\d+_([^_-]+)[_-](.*)"), split_artist_title), # See https://examine-archive.bandcamp.com/album/va-examine-archive-international-sampler-xmn01 # noqa ] # fmt: on @@ -127,7 +145,7 @@ class Helpers: @staticmethod def get_label(meta: JSONDict) -> str: try: - item = meta["albumRelease"][0]["recordLabel"] + item = meta.get("inAlbum", meta)["albumRelease"][0]["recordLabel"] except (KeyError, IndexError): item = meta["publisher"] return item.get("name") or "" @@ -147,7 +165,7 @@ def split_artists(artists: Iterable[str]) -> List[str]: """ no_ft_artists = (PATTERNS["ft"].sub("", a) for a in artists) split = map(PATTERNS["split_artists"].split, ordset(no_ft_artists)) - split_artists = ordset(map(str.strip, it.chain(*split))) - {"", "more"} + split_artists = ordset(map(str.strip, chain(*split))) - {"", "more"} for artist in list(split_artists): # ' & ' or ' X ' may be part of single artist name, so we need to be careful @@ -179,7 +197,9 @@ def parse_catalognum( cases.append((pat, "\n".join((album, disctitle, description)))) def find(pat: Pattern[str], string: str) -> str: - """Return the match if it is + """Return the match. + + It is legitimate if it is * not found in any of the track artists or titles * made of the label name when it has a space and is shorter than 6 chars """ @@ -194,39 +214,43 @@ def find(pat: Pattern[str], string: str) -> str: return "" try: - return next(filter(None, it.starmap(find, cases))) + return next(filter(None, starmap(find, cases))) except StopIteration: return "" @staticmethod def clean_name(name: str) -> str: + """Both album and track names are cleaned using these patterns.""" for pat, repl in CLEAN_PATTERNS: name = pat.sub(repl, name).strip() return name @staticmethod - def get_genre(keywords, config, label): - # type: (Iterable[str], JSONDict, str) -> Iterable[str] + def get_genre( + keywords: Iterable[str], config: JSONDict, label: str + ) -> Iterable[str]: """Return a comma-delimited list of valid genres, using MB genres for reference. - Initially, exclude keywords that are label names (unless they are valid MB genres) + 1. Exclude keywords that are label names, unless they are a valid MB genre - Verify each keyword's (potential genre) validity w.r.t. the configured `mode`: + 2. Verify each keyword's (potential genre) validity w.r.t. the configured `mode` * classical: valid only if the _entire keyword_ matches a MB genre in the list - * progressive: either above or if each of the words matches MB genre - since it - is effectively a subgenre. - * psychedelic: either one of the above or if the last word is a valid MB genre. + * progressive: either above or if each of the words matches MB genre - since + it is effectively a subgenre. + * psychedelic: one of the above or if the last word is a valid MB genre. This allows to be flexible regarding the variety of potential genres while keeping away from spammy ones. - Once we have the list of keywords that make it through the mode filters, - an additional filter is executed: - * if a keyword is _part of another keyword_ (genre within a sub-genre), - the more generic option gets excluded, for example, - >>> get_genre(['house', 'garage house', 'glitch'], "classical") - 'garage house, glitch' + 3. Once we have the list of keywords that coming out of the mode filters, + an additional filter is executed: + * if a keyword is _part of another keyword_ (genre within a sub-genre), + we keep the more specific genre, for example + >>> get_genre(["house", "garage house", "glitch"], "classical") + ["garage house", "glitch"] + + "garage house" is preferred over "house". """ - valid_mb_genre = partial(op.contains, GENRES) + valid_mb_genre = partial(contains, GENRES) label_name = label.lower().replace(" ", "") def is_label_name(kw: str) -> bool: @@ -239,7 +263,7 @@ def valid_for_mode(kw: str) -> bool: if config["mode"] == "classical": return valid_mb_genre(kw) - words = map(str.strip, kw.split(" ")) + words = map(str.strip, re.split("[ -]", kw)) if config["mode"] == "progressive": return valid_mb_genre(kw) or all(map(valid_mb_genre, words)) @@ -248,23 +272,26 @@ def valid_for_mode(kw: str) -> bool: unique_genres: ordset[str] = ordset() # expand badly delimited keywords split_kw = partial(re.split, r"[.] | #| - ") - for kw in it.chain.from_iterable(map(split_kw, keywords)): + for kw in chain.from_iterable(map(split_kw, keywords)): # remove full stops and hashes and ensure the expected form of 'and' - kw = re.sub("[.#]", "", str(kw)).replace("&", "and") - if not is_label_name(kw) and (is_included(kw) or valid_for_mode(kw)): - unique_genres.add(kw) - - def duplicate(genre: str) -> bool: - """Return True if genre is contained within another genre or if, - having removed spaces from every other, there is a duplicate found. - It is done this way so that 'dark folk' is kept while 'darkfolk' is removed, - and not the other way around. + _kw = re.sub("[.#]", "", str(kw)).replace("&", "and") + if not is_label_name(_kw) and (is_included(_kw) or valid_for_mode(_kw)): + unique_genres.add(_kw) + + def within_another_genre(genre: str) -> bool: + """Check if this genre is part of another genre. + + Remove spaces and dashes from the rest of genres and check if any of them + contain the given genre. + + This is so that 'dark folk' is kept while 'darkfolk' is removed, and not + the other way around. """ others = unique_genres - {genre} - others = others.union(x.replace(" ", "").replace("-", "") for x in others) # type: ignore[attr-defined] # noqa + others |= {x.replace(" ", "").replace("-", "") for x in others} return any(genre in x for x in others) - return it.filterfalse(duplicate, unique_genres) + return (g for g in unique_genres if not within_another_genre(g)) @staticmethod def unpack_props(obj: JSONDict) -> JSONDict: diff --git a/beetsplug/bandcamp/_metaguru.py b/beetsplug/bandcamp/metaguru.py similarity index 92% rename from beetsplug/bandcamp/_metaguru.py rename to beetsplug/bandcamp/metaguru.py index 2003eaf..97ba2da 100644 --- a/beetsplug/bandcamp/_metaguru.py +++ b/beetsplug/bandcamp/metaguru.py @@ -1,30 +1,29 @@ """Module for parsing bandcamp metadata.""" + import itertools as it import json import operator as op import re -import sys from collections import Counter from datetime import date, datetime -from functools import partial +from functools import cached_property, partial from typing import Any, Dict, Iterable, List, Optional, Set from unicodedata import normalize from beets import __version__ as beets_version from beets import config as beets_config from beets.autotag.hooks import AlbumInfo, TrackInfo +from packaging import version from pycountry import countries, subdivisions -from ._helpers import PATTERNS, Helpers, MediaInfo -from ._tracks import Track, Tracks from .album import AlbumName +from .helpers import PATTERNS, Helpers, MediaInfo +from .track import Track +from .tracks import Tracks -if sys.version_info.minor > 7: - from functools import cached_property # pylint: disable=ungrouped-imports -else: - from cached_property import cached_property # type: ignore # pylint: disable=import-error # noqa - -NEW_BEETS = int(beets_version.split(".")[1]) > 4 +BEETS_VERSION = version.parse(beets_version) +EXTENDED_FIELDS_SUPPORT = version.Version("1.5.0") <= BEETS_VERSION +ALBUMTYPES_LIST_SUPPORT = version.Version("1.6.0") < BEETS_VERSION JSONDict = Dict[str, Any] @@ -34,6 +33,7 @@ "UK": "GB", # pycountry: Great Britain "D.C.": "US", "South Korea": "KR", # pycountry: Korea, Republic of + "Turkey": "TR", # pycountry: only handles Türkiye } DATA_SOURCE = "bandcamp" WORLDWIDE = "XW" @@ -63,7 +63,7 @@ def __init__(self, meta: JSONDict, config: Optional[JSONDict] = None) -> None: self.va_name = beets_config["va_name"].as_str() or self.va_name self._tracks = Tracks.from_json(meta) self._album_name = AlbumName( - meta, self.all_media_comments, self._tracks.albums_in_titles + meta.get("name") or "", self.all_media_comments, self._tracks.album ) @classmethod @@ -80,7 +80,7 @@ def excluded_fields(self) -> Set[str]: return set(self.config.get("excluded_fields") or []) @property - def comments(self) -> str: + def comments(self) -> Optional[str]: """Return release, media descriptions and credits separated by the configured separator string. """ @@ -91,11 +91,14 @@ def comments(self) -> str: parts.append(self.meta.get("creditText") or "") sep: str = self.config["comments_separator"] - return sep.join(filter(op.truth, parts)).replace("\r", "") + return sep.join(filter(None, parts)).replace("\r", "") or None @cached_property def all_media_comments(self) -> str: - return "\n".join([*[m.description for m in self.media_formats], self.comments]) + return "\n".join([ + *[m.description for m in self.media_formats], + self.comments or "", + ]) @cached_property def label(self) -> str: @@ -188,9 +191,9 @@ def mediums(self) -> int: @cached_property def general_catalognum(self) -> str: """Find catalog number in the media-agnostic release metadata and cache it.""" - return self._tracks.single_catalognum or self.parse_catalognum( + return self._tracks.catalognum or self.parse_catalognum( album=self.meta["name"], - description=self.comments, + description=self.comments or "", label=self.label if not self._singleton else "", artistitles=self._tracks.artistitles, ) @@ -286,7 +289,7 @@ def _search_albumtype(self, word: str) -> bool: def is_single_album(self) -> bool: return ( self._singleton - or len({t.main_title for t in self.tracks}) == 1 + or len({t.title_without_remix for t in self.tracks}) == 1 or len(self._tracks.raw_names) == 1 ) @@ -348,7 +351,7 @@ def albumtype(self) -> str: return "album" @cached_property - def albumtypes(self) -> str: + def albumtypes(self) -> List[str]: albumtypes = {self.albumtype} if self.is_comp: if self.albumtype == "ep": @@ -365,7 +368,7 @@ def albumtypes(self) -> str: if len(self.tracks.remixers) == len(self.tracks): albumtypes.add("remix") - return "; ".join(sorted(albumtypes)) + return sorted(albumtypes) @cached_property def va(self) -> bool: @@ -385,7 +388,7 @@ def style(self) -> Optional[str]: @cached_property def genre(self) -> Optional[str]: - kws: Iterable[str] = map(str.lower, self.meta["keywords"]) + kws: Iterable[str] = map(str.lower, self.meta.get("keywords", [])) if self.style: exclude_style = partial(op.ne, self.style.lower()) kws = filter(exclude_style, kws) @@ -420,9 +423,11 @@ def get_fields(self, fields: Iterable[str], src: object = None) -> JSONDict: def _common_album(self) -> JSONDict: common_data: JSONDict = {"album": self.album_name} fields = ["label", "catalognum", "albumtype", "country"] - if NEW_BEETS: + if EXTENDED_FIELDS_SUPPORT: fields.extend(["genre", "style", "comments", "albumtypes"]) common_data.update(self.get_fields(fields)) + if EXTENDED_FIELDS_SUPPORT and not ALBUMTYPES_LIST_SUPPORT: + common_data["albumtypes"] = "; ".join(common_data["albumtypes"]) reldate = self.release_date if reldate: common_data.update(self.get_fields(["year", "month", "day"], reldate)) @@ -438,7 +443,7 @@ def _trackinfo(self, track: Track, **kwargs: Any) -> TrackInfo: data.pop("catalognum", None) if not data["lyrics"]: data.pop("lyrics", None) - if not NEW_BEETS: + if not EXTENDED_FIELDS_SUPPORT: data.pop("catalognum", None) data.pop("lyrics", None) for field in set(data.keys()) & self.excluded_fields: @@ -451,7 +456,7 @@ def singleton(self) -> TrackInfo: self._singleton = True self.media = self.media_formats[0] track = self._trackinfo(self.tracks.first) - if NEW_BEETS: + if EXTENDED_FIELDS_SUPPORT: track.update(self._common_album) track.pop("album", None) track.track_id = track.data_url @@ -485,10 +490,10 @@ def get_media_album(self, media: MediaInfo) -> AlbumInfo: setattr(album_info, key, val) album_info.album_id = self.media.album_id if self.media.name == "Vinyl": - album_info = self.add_track_alts(album_info, self.comments) + album_info = self.add_track_alts(album_info, self.comments or "") return album_info @cached_property - def albums(self) -> Iterable[AlbumInfo]: + def albums(self) -> List[AlbumInfo]: """Return album for the appropriate release format.""" return list(map(self.get_media_album, self.media_formats)) diff --git a/beetsplug/bandcamp/_search.py b/beetsplug/bandcamp/search.py similarity index 100% rename from beetsplug/bandcamp/_search.py rename to beetsplug/bandcamp/search.py diff --git a/beetsplug/bandcamp/track.py b/beetsplug/bandcamp/track.py index a5394ed..b1dab3a 100644 --- a/beetsplug/bandcamp/track.py +++ b/beetsplug/bandcamp/track.py @@ -1,45 +1,39 @@ """Module with a single track parsing functionality.""" + import re -import sys from dataclasses import dataclass, field -from typing import List, Optional, Tuple - +from functools import cached_property +from typing import Dict, List, Optional, Tuple -from ._helpers import CATNUM_PAT, PATTERNS, Helpers, JSONDict, _remix_pat - -if sys.version_info.minor > 7: - from functools import cached_property # pylint: disable=ungrouped-imports -else: - from cached_property import cached_property # type: ignore # pylint: disable=import-error # noqa +from .helpers import CATNUM_PAT, PATTERNS, REMIX, Helpers, JSONDict digiwords = r""" # must contain at least one of - (\W*(bandcamp|digi(tal)?|exclusive|bonus|bns|unreleased))+ + ([ -]? # delimiter + (bandcamp|digi(tal)?|exclusive|bonus|bns|unreleased) + )+ # and may be followed by (\W(track|only|tune))* """ DIGI_ONLY_PATTERN = re.compile( rf""" -\s* # all preceding space +(\s|[^][()\w])* # space or anything that is not a parens or an alphabetical char ( (^{digiwords}[.:\d\s]+\s) # begins with 'Bonus.', 'Bonus 1.' or 'Bonus :' | [\[(]{digiwords}[\])]\W* # delimited by brackets, '[Bonus]', '(Bonus) -' - | [*]{digiwords}[*] # delimited by asterisks, '*Bonus*' + | [*]{digiwords}[*]? # delimited by asterisks, '*Bonus', '*Bonus*' + | {digiwords}[ ]- # followed by ' -', 'Bonus -' | ([ ]{digiwords}$) # might not be delimited if at the end, '... Bonus' ) \s* # all succeeding space """, re.I | re.VERBOSE, ) -DELIMITER_PAT = re.compile(r" ([^\w&()+/[\] ]) ") -ELP_ALBUM_PAT = re.compile(r"[- ]*\[([^\]]+ [EL]P)\]+") # Title [Some Album EP] -TITLE_IN_QUOTES = re.compile(r'^(.+[^ -])[ -]+"([^"]+)"$') -NUMBER_PREFIX = re.compile(r"(^|- )\d{2,}\W* ") @dataclass class Remix: - PATTERN = re.compile(rf" *[\[(] *{_remix_pat}[])]", re.I) + PATTERN = re.compile(rf" *[\[(] *{REMIX.pattern}[])]", re.I) delimited: str remixer: str @@ -64,31 +58,15 @@ class Track: index: Optional[int] = None json_artist: str = "" - _name: str = "" + name: str = "" ft: str = "" - album: str = "" - catalognum: str = "" + catalognum: Optional[str] = None + ft_artist: str = "" remix: Optional[Remix] = None digi_only: bool = False track_alt: Optional[str] = None - @classmethod - def from_json(cls, json: JSONDict, delim: str, label: str) -> "Track": - try: - artist = json["inAlbum"]["byArtist"]["name"] - except KeyError: - artist = "" - artist = artist or json.get("byArtist", {}).get("name", "") - data = { - "json_item": json, - "json_artist": artist, - "track_id": json["@id"], - "index": json.get("position"), - "catalognum": json["name_parts"].get("catalognum", ""), - } - return cls(**cls.parse_name(data, json["name_parts"]["clean"], delim, label)) - @staticmethod def clean_digi_name(name: str) -> Tuple[str, bool]: """Clean the track title from digi-only artifacts. @@ -99,7 +77,16 @@ def clean_digi_name(name: str) -> Tuple[str, bool]: return clean_name, clean_name != name @staticmethod - def find_featuring(data: JSONDict) -> JSONDict: + def split_ft(value: str) -> Tuple[str, str, str]: + """Return ft artist, full ft string, and the value without the ft string.""" + if m := PATTERNS["ft"].search(value): + grp = m.groupdict() + return grp["ft_artist"], grp["ft"], value.replace(m.group(), "") + + return "", "", value + + @classmethod + def get_featuring_artist(cls, name: str, artist: str) -> Dict[str, str]: """Find featuring artist in the track name. If the found artist is contained within the remixer, do not do anything. @@ -107,60 +94,65 @@ def find_featuring(data: JSONDict) -> JSONDict: do not consider it as a featuring artist. Otherwise, strip brackets and spaces and save it in the 'ft' field. """ - for _field in "_name", "json_artist": - m = PATTERNS["ft"].search(data[_field]) - if m: - ft = m.groups()[-1].strip() - if ft not in data.get("remixer", ""): - data[_field] = data[_field].replace(m.group().rstrip(), "") - if ft not in data["json_artist"]: - data["ft"] = m.group().strip(" ([])") - break - return data - - @staticmethod - def parse_name(data: JSONDict, name: str, delim: str, label: str) -> JSONDict: - name = name.replace(f" {delim} ", " - ") + ft_artist, ft, name = cls.split_ft(name) - # remove label from the end of the track name - # see https://gutterfunkuk.bandcamp.com/album/gutterfunk-all-subject-to-vibes-various-artists-lp # noqa - if name.endswith(label): - name = name.replace(label, "").strip(" -") + if not ft_artist: + ft_artist, ft, artist = cls.split_ft(artist) - json_artist, artist_digi_only = Track.clean_digi_name(data["json_artist"]) - name, name_digi_only = Track.clean_digi_name(name) - data["digi_only"] = name_digi_only or artist_digi_only + return {"name": name, "json_artist": artist, "ft": ft, "ft_artist": ft_artist} - data["json_artist"] = Helpers.clean_name(json_artist) if json_artist else "" + @classmethod + def parse_name(cls, name: str, artist: str, index: Optional[int]) -> JSONDict: + result: JSONDict = {} + artist, artist_digi_only = cls.clean_digi_name(artist) + name, name_digi_only = cls.clean_digi_name(name) + result["digi_only"] = name_digi_only or artist_digi_only + + if artist: + artist = Helpers.clean_name(artist) name = Helpers.clean_name(name).strip().lstrip("-") + # find the track_alt and remove it from the name m = PATTERNS["track_alt"].search(name) if m: - data["track_alt"] = m.group(1).replace(".", "").upper() + result["track_alt"] = m.group(1).replace(".", "").upper() name = name.replace(m.group(), "") - if not data.get("catalognum"): - # check whether track name contains the catalog number within parens - # or square brackets - # see https://objection999x.bandcamp.com/album/eruption-va-obj012 - m = CATNUM_PAT["delimited"].search(name) - if m: - data["catalognum"] = m.group(1) - name = name.replace(m.group(), "").strip() - name = re.sub(rf"^0*{data.get('index', 0)}(?!\W\d)\W+", "", name) + # check whether track name contains the catalog number within parens + # or square brackets + # see https://objection999x.bandcamp.com/album/eruption-va-obj012 + m = CATNUM_PAT["delimited"].search(name) + if m: + result["catalognum"] = m.group(1) + name = name.replace(m.group(), "").strip() + + # Remove leading index + if index: + name = re.sub(rf"^0*{index}(?!\W\d)\W+", "", name) + # find the remixer and remove it from the name remix = Remix.from_name(name) if remix: - data.update(remix=remix) + result["remix"] = remix name = name.replace(remix.delimited, "").rstrip() - for m in ELP_ALBUM_PAT.finditer(name): - data["album"] = m.group(1).replace('"', "") - name = name.replace(m.group(), "") + return {**result, **cls.get_featuring_artist(name, artist)} + + @classmethod + def make(cls, json: JSONDict, name: str) -> "Track": + try: + artist = json["inAlbum"]["byArtist"]["name"] + except KeyError: + artist = json.get("byArtist", {}).get("name", "") - data["_name"] = name - data = Track.find_featuring(data) - return data + index = json.get("position") + data = { + "json_item": json, + "track_id": json["@id"], + "index": index, + **cls.parse_name(name, artist, index), + } + return cls(**data) @cached_property def duration(self) -> Optional[int]: @@ -181,42 +173,41 @@ def lyrics(self) -> str: return text.replace("\r", "") @cached_property - def name(self) -> str: - name = self._name + def full_name(self) -> str: + name = self.name if self.json_artist and " - " not in name: name = f"{self.json_artist} - {name}" return name.strip() @cached_property - def main_title(self) -> str: + def title_without_remix(self) -> str: """Split the track name, deduce the title and return it. + The extra complexity here is to ensure that it does not cut off a title that ends with ' - -', like in '(DJ) NICK JERSEY - 202memo - - -'. """ - parts = re.split(r" - (?![^\[(]+[])])", self.name) + parts = re.split(r" - (?![^\[(]+[])])", self.full_name) if len(parts) == 1: - parts = self.name.split(" - ") - main_title = parts[-1] + parts = self.full_name.split(" - ") + title_without_remix = parts[-1] for idx, maybe in enumerate(reversed(parts)): if not maybe.strip(" -"): - main_title = " - ".join(parts[-idx - 2 :]) + title_without_remix = " - ".join(parts[-idx - 2 :]) break - return main_title + return title_without_remix @cached_property def title(self) -> str: """Return the main title with the full remixer part appended to it.""" if self.remix: - return f"{self.main_title} {self.remix.delimited}" - return self.main_title + return f"{self.title_without_remix} {self.remix.delimited}" + return self.title_without_remix @cached_property def artist(self) -> str: - """Take the name, remove the title, ensure it does not duplicate any remixers - and return the resulting artist. - """ - artist = self.name[: self.name.rfind(self.main_title)].strip(", -") - artist = Remix.PATTERN.sub("", artist) + """Return name without the title and the remixer.""" + title_start_idx = self.full_name.rfind(self.title_without_remix) + artist = Remix.PATTERN.sub("", self.full_name[:title_start_idx].strip(", -")) if self.remix: artist = artist.replace(self.remix.remixer, "").strip(" ,") return artist.strip(" -") @@ -232,7 +223,11 @@ def info(self) -> JSONDict: "medium_index": self.index, "medium": None, "track_id": self.track_id, - "artist": self.artist + (f" {self.ft}" if self.ft else ""), + "artist": ( + f"{self.artist} {self.ft}" + if self.ft_artist not in self.artist + self.title + else self.artist + ), "title": self.title, "length": self.duration, "track_alt": self.track_alt, diff --git a/beetsplug/bandcamp/track_names.py b/beetsplug/bandcamp/track_names.py new file mode 100644 index 0000000..0036bc5 --- /dev/null +++ b/beetsplug/bandcamp/track_names.py @@ -0,0 +1,155 @@ +"""Module for parsing track names.""" + +import operator as op +import re +from collections import Counter +from contextlib import suppress +from dataclasses import dataclass +from functools import reduce +from typing import Iterator, List, Optional, Tuple + +from ordered_set import OrderedSet + +from .helpers import CATNUM_PAT, REMIX + + +@dataclass +class TrackNames: + """Responsible for parsing track names in the entire release context.""" + + # Title [Some Album EP] + ALBUM_IN_TITLE = re.compile(r"[- ]*\[([^\]]+ [EL]P)\]+", re.I) + DELIMITER_PAT = re.compile(r" ([^\w&()+/[\] ]) ") + TITLE_IN_QUOTES = re.compile(r'^(.+[^ -])[ -]+"([^"]+)"$') + NUMBER_PREFIX = re.compile(r"((?<=^)|(?<=- ))\d{2,}\W* ") + + original: List[str] + names: List[str] + album: Optional[str] = None + catalognum: Optional[str] = None + + def __iter__(self) -> Iterator[str]: + return iter(self.names) + + @classmethod + def split_quoted_titles(cls, names: List[str]) -> List[str]: + if len(names) > 1: + matches = list(filter(None, map(cls.TITLE_IN_QUOTES.match, names))) + if len(matches) == len(names): + return [m.expand(r"\1 - \2") for m in matches] + + return names + + @classmethod + def remove_number_prefix(cls, names: List[str]) -> List[str]: + prefix_matches = [cls.NUMBER_PREFIX.search(n) for n in names] + if all(prefix_matches): + prefixes = [m.group() for m in prefix_matches] # type: ignore[union-attr] + if len(set(prefixes)) > 1: + return [n.replace(p, "") for n, p in zip(names, prefixes)] + + return names + + @classmethod + def find_common_track_delimiter(cls, names: List[str]) -> str: + """Return the track parts delimiter that is in effect in the current release. + + In some (rare) situations track parts are delimited by a pipe character + or some UTF-8 equivalent of a dash. + + This checks every track for the first character (see the regex for exclusions) + that splits it. The character that splits the most and at least half of + the tracks is the character we need. + + If no such character is found, or if we have just one track, return a dash '-'. + """ + + def get_delim(string: str) -> str: + m = cls.DELIMITER_PAT.search(string) + return m.group(1) if m else "-" + + delim, count = Counter(map(get_delim, names)).most_common(1).pop() + return delim if (len(names) == 1 or count > len(names) / 2) else "-" + + @classmethod + def normalize_delimiter(cls, names: List[str]) -> List[str]: + """Ensure the same delimiter splits artist and title in all names.""" + delim = cls.find_common_track_delimiter(names) + return [n.replace(f" {delim} ", " - ") for n in names] + + @staticmethod + def remove_label(names: List[str], label: str) -> List[str]: + """Remove label name from the end of track names. + + See https://gutterfunkuk.bandcamp.com/album/gutterfunk-all-subject-to-vibes-various-artists-lp + """ + return [ + (n.replace(label, "").strip(" -") if n.endswith(label) else n) + for n in names + ] + + @staticmethod + def eject_common_catalognum(names: List[str]) -> Tuple[Optional[str], List[str]]: + """Return catalognum found in every track title. + + 1. Split each track name into words + 2. Find the list of words that are common to all tracks + 3. Check the *first* and the *last* word for the catalog number + - If found, return it and remove it from every track name + """ + catalognum = None + + names_tokens = map(str.split, names) + common_words = reduce(op.and_, [OrderedSet(x) for x in names_tokens]) + if common_words: + matches = (CATNUM_PAT["anywhere"].search(common_words[i]) for i in [0, -1]) + with suppress(StopIteration): + catalognum, word = next((m.group(1), m.string) for m in matches if m) + names = [n.replace(word, "").strip() for n in names] + + return catalognum, names + + @staticmethod + def parenthesize_remixes(names: List[str]) -> List[str]: + """Reformat broken remix titles for an album with a single root title. + + 1. Check whether this release has a single root title + 2. Find remixes that do not have parens around them + 3. Add parens + """ + names_tokens = map(str.split, names) + common_words = reduce(op.and_, [OrderedSet(x) for x in names_tokens]) + joined = " ".join(common_words) + if joined in names: # it is one of the track names (root title) + remix_parts = [n.replace(joined, "").lstrip() for n in names] + return [ + (n.replace(rp, f"({rp})") if REMIX.fullmatch(rp) else n) + for n, rp in zip(names, remix_parts) + ] + + return names + + @classmethod + def eject_album_name(cls, names: List[str]) -> Tuple[Optional[str], List[str]]: + matches = list(map(cls.ALBUM_IN_TITLE.search, names)) + albums = {m.group(1).replace('"', "") for m in matches if m} + if len(albums) != 1: + return None, names + + return albums.pop(), [ + (n.replace(m.group(), "") if m else n) for m, n in zip(matches, names) + ] + + @classmethod + def make(cls, original: List[str], label: str) -> "TrackNames": + names = cls.parenthesize_remixes( + cls.remove_label( + cls.normalize_delimiter( + cls.remove_number_prefix(cls.split_quoted_titles(original)) + ), + label, + ) + ) + catalognum, names = cls.eject_common_catalognum(names) + album, names = cls.eject_album_name(names) + return cls(original, names, album=album, catalognum=catalognum) diff --git a/beetsplug/bandcamp/tracks.py b/beetsplug/bandcamp/tracks.py new file mode 100644 index 0000000..b059be0 --- /dev/null +++ b/beetsplug/bandcamp/tracks.py @@ -0,0 +1,130 @@ +"""Module with tracks parsing functionality.""" + +import itertools as it +from dataclasses import dataclass +from functools import cached_property +from itertools import starmap +from typing import Iterator, List, Optional, Set + +from .helpers import Helpers, JSONDict +from .track import Track +from .track_names import TrackNames + + +@dataclass +class Tracks: + tracks: List[Track] + names: TrackNames + + def __iter__(self) -> Iterator[Track]: + return iter(self.tracks) + + def __len__(self) -> int: + return len(self.tracks) + + @classmethod + def from_json(cls, meta: JSONDict) -> "Tracks": + try: + tracks = [{**t, **t["item"]} for t in meta["track"]["itemListElement"]] + except (TypeError, KeyError): + tracks = [meta] + + names = TrackNames.make( + [i.get("name", "") for i in tracks], Helpers.get_label(meta) + ) + return cls(list(starmap(Track.make, zip(tracks, names))), names) + + @property + def album(self) -> Optional[str]: + return self.names.album + + @property + def catalognum(self) -> Optional[str]: + return self.names.catalognum + + @cached_property + def first(self) -> Track: + return self.tracks[0] + + @cached_property + def raw_names(self) -> List[str]: + return [j.name for j in self.tracks] + + @cached_property + def original_artists(self) -> List[str]: + """Return all unique unsplit (original) main track artists.""" + return list(dict.fromkeys(j.artist for j in self.tracks)) + + @property + def artists(self) -> List[str]: + """Return all unique split main track artists. + + "Artist1 x Artist2" -> ["Artist1", "Artist2"] + """ + return list(dict.fromkeys(it.chain(*(j.artists for j in self.tracks)))) + + @property + def remixers(self) -> List[str]: + """Return all remix artists.""" + return [ + t.remix.remixer + for t in self.tracks + if t.remix and not t.remix.by_other_artist + ] + + @property + def other_artists(self) -> Set[str]: + """Return all unique remix and featuring artists.""" + ft = [j.ft for j in self.tracks if j.ft] + return set(it.chain(self.remixers, ft)) + + @cached_property + def all_artists(self) -> Set[str]: + """Return all unique (1) track, (2) remix, (3) featuring artists.""" + return self.other_artists | set(self.original_artists) + + @cached_property + def artistitles(self) -> str: + """Returned artists and titles joined into one long string.""" + return " ".join(it.chain(self.raw_names, self.all_artists)).lower() + + def adjust_artists(self, albumartist: str) -> None: + """Handle some track artist edge cases. + + These checks require knowledge of the entire release, therefore cannot be + performed within the context of a single track. + + * When artist name is mistaken for the track_alt + * When artist and title are delimited by '-' without spaces + * When artist and title are delimited by a UTF-8 dash equivalent + * Defaulting to the album artist + """ + track_alts = {t.track_alt for t in self.tracks if t.track_alt} + artists = [t.artist for t in self.tracks if t.artist] + + for t in [track for track in self.tracks if not track.artist]: + if t.track_alt and len(track_alts) == 1: # only one track_alt + # the only track that parsed a track alt - it's most likely a mistake + # one artist was confused for a track alt, like 'B2', - reverse this + t.artist, t.track_alt = t.track_alt, None + elif len(artists) == len(self) - 1: # only 1 missing artist + # if this is a remix and the parsed title is part of the albumartist or + # is one of the track artists, we made a mistake parsing the remix: + # it is most probably the edge case where the `title_without_remix` is a + # legitimate artist and the track title is something like 'Hello Remix' + if t.remix and (t.title_without_remix in albumartist): + t.artist, t.title = t.title_without_remix, t.remix.remix + # this is the only artist that didn't get parsed - relax the rule + # and try splitting with '-' without spaces + split = t.title.split("-") + if len(split) == 1: + # attempt to split by another ' ? ' where '?' may be some utf-8 + # alternative of a dash + split = [ + s for s in TrackNames.DELIMITER_PAT.split(t.title) if len(s) > 1 + ] + if len(split) > 1: + t.artist, t.title = split + if not t.artist: + # default to the albumartist + t.artist = albumartist diff --git a/poetry.lock b/poetry.lock index 44c78cd..6ff3fda 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,51 +1,42 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "astroid" -version = "2.11.7" +version = "3.1.0" description = "An abstract syntax tree for Python with inference support." -category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.8.0" files = [ - {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"}, - {file = "astroid-2.11.7.tar.gz", hash = "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"}, + {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, + {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, ] [package.dependencies] -lazy-object-proxy = ">=1.4.0" -setuptools = ">=20.0" -typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} -wrapt = ">=1.11,<2" +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "beets" version = "1.6.0" description = "music tagger and library organizer" -category = "main" optional = false python-versions = "*" files = [ @@ -87,120 +78,120 @@ test = ["beautifulsoup4", "coverage", "flask", "mock", "py7zr", "pylast", "pytes thumbnails = ["Pillow", "pyxdg"] web = ["flask", "flask-cors"] -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, - {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, -] - [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -212,7 +203,6 @@ files = [ name = "confuse" version = "2.0.1" description = "Painless YAML configuration." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -225,72 +215,63 @@ pyyaml = "*" [[package]] name = "coverage" -version = "7.2.7" +version = "7.5.0" description = "Code coverage measurement for Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, + {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, + {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, + {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, + {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, + {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, + {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, + {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, + {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, + {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, + {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, + {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, + {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, ] [package.dependencies] @@ -301,24 +282,23 @@ toml = ["tomli"] [[package]] name = "dill" -version = "0.3.7" +version = "0.3.8" description = "serialize all of Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, - {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "eradicate" version = "2.3.0" description = "Removes commented-out code." -category = "dev" optional = false python-versions = "*" files = [ @@ -328,24 +308,36 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.2.1" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.1" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "flake8" version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -354,21 +346,19 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.9.0,<2.10.0" pyflakes = ">=2.5.0,<2.6.0" [[package]] name = "flake8-bugbear" -version = "22.12.6" +version = "23.3.12" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "flake8-bugbear-22.12.6.tar.gz", hash = "sha256:4cdb2c06e229971104443ae293e75e64c6107798229202fbe4f4091427a30ac0"}, - {file = "flake8_bugbear-22.12.6-py3-none-any.whl", hash = "sha256:b69a510634f8a9c298dfda2b18a8036455e6b19ecac4fe582e4d7a0abfa50a30"}, + {file = "flake8-bugbear-23.3.12.tar.gz", hash = "sha256:e3e7f74c8a49ad3794a7183353026dabd68c74030d5f46571f84c1fb0eb79363"}, + {file = "flake8_bugbear-23.3.12-py3-none-any.whl", hash = "sha256:beb5c7efcd7ccc2039ef66a77bb8db925e7be3531ff1cb4d0b7030d0e2113d72"}, ] [package.dependencies] @@ -376,79 +366,90 @@ attrs = ">=19.2.0" flake8 = ">=3.0.0" [package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "tox"] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] [[package]] name = "flake8-comprehensions" -version = "3.13.0" +version = "3.14.0" description = "A flake8 plugin to help you write better list/set/dict comprehensions." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "flake8_comprehensions-3.13.0-py3-none-any.whl", hash = "sha256:cc0d6dbb336ff4e9cdf4eb605a3f719ea59261f2d6ba52034871a173c40e1f60"}, - {file = "flake8_comprehensions-3.13.0.tar.gz", hash = "sha256:83cf98e816c9e23360f36aaf47de59a5b21437fdff8a056c46e2ad49f81861bf"}, + {file = "flake8_comprehensions-3.14.0-py3-none-any.whl", hash = "sha256:7b9d07d94aa88e62099a6d1931ddf16c344d4157deedf90fe0d8ee2846f30e97"}, + {file = "flake8_comprehensions-3.14.0.tar.gz", hash = "sha256:81768c61bfc064e1a06222df08a2580d97de10cb388694becaf987c331c6c0cf"}, ] [package.dependencies] flake8 = ">=3.0,<3.2.0 || >3.2.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "flake8-eradicate" -version = "1.4.0" +version = "1.5.0" description = "Flake8 plugin to find commented out code" -category = "dev" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8,<4.0" files = [ - {file = "flake8-eradicate-1.4.0.tar.gz", hash = "sha256:3088cfd6717d1c9c6c3ac45ef2e5f5b6c7267f7504d5a74b781500e95cb9c7e1"}, - {file = "flake8_eradicate-1.4.0-py3-none-any.whl", hash = "sha256:e3bbd0871be358e908053c1ab728903c114f062ba596b4d40c852fd18f473d56"}, + {file = "flake8_eradicate-1.5.0-py3-none-any.whl", hash = "sha256:18acc922ad7de623f5247c7d5595da068525ec5437dd53b22ec2259b96ce9d22"}, + {file = "flake8_eradicate-1.5.0.tar.gz", hash = "sha256:aee636cb9ecb5594a7cd92d67ad73eb69909e5cc7bd81710cf9d00970f3983a6"}, ] [package.dependencies] attrs = "*" eradicate = ">=2.0,<3.0" -flake8 = ">=3.5,<6" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +flake8 = ">5" [[package]] name = "idna" -version = "3.4" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "importlib-metadata" -version = "4.2.0" +version = "7.1.0" description = "Read metadata from Python packages" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "importlib-resources" +version = "5.13.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-5.13.0-py3-none-any.whl", hash = "sha256:9f7bd0c97b79972a6cce36a366356d16d5e13b09679c11a58f1014bfdf8e64b2"}, + {file = "importlib_resources-5.13.0.tar.gz", hash = "sha256:82d5c6cca930697dbbd86c93333bb2c2e72861d4789a11c2662b933e5ad2b528"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -458,150 +459,105 @@ files = [ [[package]] name = "isort" -version = "5.11.5" +version = "5.13.2" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "isort-5.11.5-py3-none-any.whl", hash = "sha256:ba1d72fb2595a01c7895a5128f9585a5cc4b6d395f1c8d514989b9a7eb2a8746"}, - {file = "isort-5.11.5.tar.gz", hash = "sha256:6be1f76a507cb2ecf16c7cf14a37e41609ca082330be4e3436a18ef74add55db"}, + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] [package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] +colors = ["colorama (>=0.4.6)"] [[package]] name = "jellyfish" -version = "1.0.0" +version = "1.0.3" description = "Approximate and phonetic matching of strings." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "jellyfish-1.0.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:26f8fbcc112e6f61c03446860e649b2341acfdda80d1fd53dd67887a409b6785"}, - {file = "jellyfish-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f9b48db1fba511726e95f19de29a1345c2a0762d880f9723ab0a0de847fcfcfc"}, - {file = "jellyfish-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23b68747fda53fb27b4948caa6a32754ce7d3c5d22e9b3aeae25cae138830cd7"}, - {file = "jellyfish-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:822f0e7d98e8a1bbf2ec2735d9ab5a9f74cd33248953f5fac692d93230256a4f"}, - {file = "jellyfish-1.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1f902e2d4298f951a3185fea13b0f4dd44ed8ce9902ae3c1ea8d30e5e5194ebd"}, - {file = "jellyfish-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:826860f21a1dfe92856f7b9a1df03005ef5a6cc96a34a5e33e723ba45fb53548"}, - {file = "jellyfish-1.0.0-cp310-none-win32.whl", hash = "sha256:54cb4e11378e48ca481e9713a8a71a7e7ead9a035927b854f102d01c84c6b2e6"}, - {file = "jellyfish-1.0.0-cp310-none-win_amd64.whl", hash = "sha256:201ef2a4274971ce056c8dff48700c88c00bc61b55af0b1f12d2bac34c7522f4"}, - {file = "jellyfish-1.0.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:a2a678cdc805e6e19bc150921d162ca4a87b6ab29d9dcb5edff8973fcde60706"}, - {file = "jellyfish-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b01306cbe0196469be9a6b9f0c9b0d6de672c56f2955b52850f8fa9a08d22d71"}, - {file = "jellyfish-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e56bccafdb3e3710f85d25c052e8f20ef92f4bab2d79358df90dfaf8561a728"}, - {file = "jellyfish-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9caae1ab4bb1878277330f88cb414685fec21fe90bc44f55fb651b8227908ffb"}, - {file = "jellyfish-1.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5d87d1c8c5500b862ac885ae85c5bed9e0d5a8aadfc9ff600a41dffd9dd86f39"}, - {file = "jellyfish-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f6e85c00dece851f6daff9f093bec589fb74e5964077bbc5bd14bd1574c3c00c"}, - {file = "jellyfish-1.0.0-cp311-none-win32.whl", hash = "sha256:44a7275d6d03835cb9a55269da453eba1e2ad9b9ece783d29b51ec5152b3ac42"}, - {file = "jellyfish-1.0.0-cp311-none-win_amd64.whl", hash = "sha256:8855645f4ae85e1b3a4279623c53c1ad1d9c2b40db904e0d1102233d8cbc7617"}, - {file = "jellyfish-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d48c9fc29ae401250386a1d0b45596288b8e958a8944cb4b2e9515debd14fa7c"}, - {file = "jellyfish-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28af35fdd0b37af0240560e6a061aa25fcedc7b4600dd718acd4f4b41825b7f2"}, - {file = "jellyfish-1.0.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:e593eb939cd99765a6e2e04c69909c0825badfd3f9a78e9129047f91e3fd00ce"}, - {file = "jellyfish-1.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:b6f0b5c8681e99b61b47964fdf280073f1b41003cd36cd05964e07931fd04607"}, - {file = "jellyfish-1.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f875ba01de9bc81181733301f4589462c087e6b28734ff5f2306e2a4c54515d3"}, - {file = "jellyfish-1.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7ddf60f4dcf0d45ef125ca4ddf41551ceb7f7acd6a60896d04b814902749e86"}, - {file = "jellyfish-1.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:48c1a4a1de3b2d9a51e15ede7fc1b719cd736235d9a3e7a1a8d24daae5d73e4d"}, - {file = "jellyfish-1.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a657869f0a73195517405cc9cbd97dd6c01a0ce0a64f99a1e6983f0dbe14ed34"}, - {file = "jellyfish-1.0.0-cp37-none-win32.whl", hash = "sha256:6c97bc0c24d745667c411ca6204cb4fbbb4280679b85747c5214544387a9caa6"}, - {file = "jellyfish-1.0.0-cp37-none-win_amd64.whl", hash = "sha256:775f624a1cfdc2bc5e78f15c13dc37e21aee106128e68428906a39c3db8fb2b4"}, - {file = "jellyfish-1.0.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:00047f10081bb16682893817b767a52844926dec516a89dcdfb7f958d3e91cf6"}, - {file = "jellyfish-1.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:536953db6900bc0f192bb2bede11e6b46c011dfcd0c6bec52768fe6e8861e3be"}, - {file = "jellyfish-1.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80ae9f7d7f3bfffd45171590642bba5a80d917db84129d99fa2d9fbfb3ff1b4e"}, - {file = "jellyfish-1.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d8b47d4e5876e7f2baed994e772b6cd88d8e7db4216daca6a7a9bc3764a36c0"}, - {file = "jellyfish-1.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0f90504a64e393d7f6409a88d2f7c84cf13e5e58a96365b09d9d6712d95545cd"}, - {file = "jellyfish-1.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0da4c4c3e9a170d6b410ce404ab6021150914ec603bd50359b311738c45a8e0"}, - {file = "jellyfish-1.0.0-cp38-none-win32.whl", hash = "sha256:bb7e6ba7fde138aeb74da92da515674884a7405df847d187c81a7bb722747a3d"}, - {file = "jellyfish-1.0.0-cp38-none-win_amd64.whl", hash = "sha256:ee1b55bd898a5e777da1f0b9809f43a0443a8dcb9bf86a85a76fe24aee91bb7e"}, - {file = "jellyfish-1.0.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:383c4a5e961d87c349f7e60824b2c777d4437c73afbd369688dee26f59ef1c13"}, - {file = "jellyfish-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:088845e6e3c1ade37783d88b8f6936ee1f6750e50bfb9c6d372ff26cd39083d2"}, - {file = "jellyfish-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cff17fb418699405eb72599d28f11d4b76d54941f56395353bc83a379e6082cf"}, - {file = "jellyfish-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fee1e31df9a1406533766cab4d5027bef05d863c03202bef8558144997a020a4"}, - {file = "jellyfish-1.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fecec758a5a0ce956d6f9aa55c0d8568fe14a8ff589baba2162d3b5d2856ff9e"}, - {file = "jellyfish-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2edbd782e93a3f5d36d1c835ca3a5a98c14faa0d349888827f87ce57d33a3808"}, - {file = "jellyfish-1.0.0-cp39-none-win32.whl", hash = "sha256:36ea675a448f155aa57cc2d54a7f188c509771068db883df359e9fe4f0024b7b"}, - {file = "jellyfish-1.0.0-cp39-none-win_amd64.whl", hash = "sha256:9da47c62e32a2ea0b16199e952ddb9ff15cbcce17fc5385d78c6a599ba2b9134"}, - {file = "jellyfish-1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1f7c72bfb2df2b48f0bde483fc4b57e55d6cfb604d03472cebb060269df0b9"}, - {file = "jellyfish-1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0562c102a2994c0ced8fe684340d82cfd8f4247ee32b9e46a53221b027c4ef51"}, - {file = "jellyfish-1.0.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e09cde1c54a904c9d8eb30cd82a1722d076c89b1e91ab452b4b552e0185eb655"}, - {file = "jellyfish-1.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:c044bc2338a9040f5996a8583596bb10850dfe03156c93b797605407f26510f8"}, - {file = "jellyfish-1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c531cdfdfd40734c296c6c2d0dc346d9bd6e276decea76ac3409fe7adca133d"}, - {file = "jellyfish-1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a73abd3da659eb227afc10d1c5801c72368cf2553219424afe317f376bec0256"}, - {file = "jellyfish-1.0.0-pp38-pypy38_pp73-musllinux_1_1_i686.whl", hash = "sha256:89440574d5256f97d7b4e05547f4354c698f7f79b15b5233a95c67c14bfb7cb8"}, - {file = "jellyfish-1.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0235508b78920ce1b8c1c433c87b807e6b576442e1c53d7fea72451ff3c02d2f"}, - {file = "jellyfish-1.0.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be3da1f33dd1c844a7ef05ec7b5880b1e17fea6563ccfae50bf0e02cbdce4265"}, - {file = "jellyfish-1.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d29c41fb5108af0b48ae8c603af9798011e25f809b45e647f9a268deb7ead533"}, - {file = "jellyfish-1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4d9058441bb52c783179d2a636739c0b4ab007f3c064f8095fd451d1f814d2b"}, - {file = "jellyfish-1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f9320118a3be1c3d9312fbf9e943672f524c5aa1abeda8334d835bd9ad6d2e"}, - {file = "jellyfish-1.0.0-pp39-pypy39_pp73-musllinux_1_1_i686.whl", hash = "sha256:aa6b05cfaeb22643db0c7b38d97b9b854504d2fad3358761fb99b5ef84e0384d"}, - {file = "jellyfish-1.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:969359187749fa7b2cefdc8bdc18b48525fd676fc1632c453bd7ca1e41810066"}, - {file = "jellyfish-1.0.0.tar.gz", hash = "sha256:881aae3671999bb07ce50c27696f29ca7e842f3e123acc0db6525c9999881bd4"}, -] - -[[package]] -name = "lazy-object-proxy" -version = "1.9.0" -description = "A fast and thorough lazy object proxy." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, + {file = "jellyfish-1.0.3-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:318169ed003949a177536f9d6ee240997056caef75e37a66e7ade1e7d6e9fa4a"}, + {file = "jellyfish-1.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0281e85aaa1a6edead83a66801a3ad24da27f80bee1efc6ff85ac7fa10069270"}, + {file = "jellyfish-1.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54c862cbc866aa45521d9a01100c1a3d7b305ebde2b4d7606cbd6c82350fdfa0"}, + {file = "jellyfish-1.0.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee1a1db7bb5c240031d7769301fd5291a40997df76e68d322a2aa9b4f3fd7ba4"}, + {file = "jellyfish-1.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:face52972a7c8d2538aec5bdbc4c863a7f2b3c5def88a6e6977fcaec8acefc92"}, + {file = "jellyfish-1.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:643315eb140af1bbfffb6d38b0371920c875ca3ec3fa0403b851bd50b9a9603c"}, + {file = "jellyfish-1.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5d01941cd21ff57e41713c77786654f513cca2ace73f7ad53aefdf952600d06"}, + {file = "jellyfish-1.0.3-cp310-none-win32.whl", hash = "sha256:5932f7d0643db9deb09de50770fa7fd872f4e76d39686568d2a63bd3e4af16bb"}, + {file = "jellyfish-1.0.3-cp310-none-win_amd64.whl", hash = "sha256:da254a29f9ea2c5548fe02e42d0d361de566a92c6e7aef991eb6640d18b6f4b5"}, + {file = "jellyfish-1.0.3-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:06d75c438a735a689986ba6a199daa60da30515dd99696b08c10c800b08a8725"}, + {file = "jellyfish-1.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:abd8388161df3ec6469b7b6fdad1066efb3958d885b7390ed105824c7e4ac68f"}, + {file = "jellyfish-1.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61f0317789d139456c678f7a68a93130d3c0227e76d200cbeedf156719747d8f"}, + {file = "jellyfish-1.0.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:553f195f2d24702b8586c6bd73a938c028d417f58ca4aaf735d827db7e21e827"}, + {file = "jellyfish-1.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aea474cf64998eb102d622944fde3d9198b17a2ec36ed1cae34fdb1887015fb"}, + {file = "jellyfish-1.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:65e4256c44963337fcc63897f01f4a754bdc17e4d740bceb55c04ab94ca9c07e"}, + {file = "jellyfish-1.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0ad15b58b808c76082b19c606a7a32d4c2c92520bbcb3a5c637e36520248e18b"}, + {file = "jellyfish-1.0.3-cp311-none-win32.whl", hash = "sha256:0a5fff421ec821d37ad316f1f8e7706c5868d662d1dadcdbefd38ba1df45152d"}, + {file = "jellyfish-1.0.3-cp311-none-win_amd64.whl", hash = "sha256:c53895523a7a911621c4f46ffafb234ddbcd467a0e63782304c68652ba8dea41"}, + {file = "jellyfish-1.0.3-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:7bf482b872dea6267fd486957bb018c69f11618130f0e2e2cd04a9dd64be32f5"}, + {file = "jellyfish-1.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e162ab1c140a6ceb44a7d13e53b94a0ad5f796a4b1389bb91dc6448f7cfecd73"}, + {file = "jellyfish-1.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ce7aeb268acecec02f8f863a8343641064cb83d3d2fc44f4b2ca9fb31a1395d"}, + {file = "jellyfish-1.0.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:484051e6eaa55063e8683dca4336acff5d66c794b8517d569f4055e3df38a789"}, + {file = "jellyfish-1.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a53b8636935f169bf928140ff09207c3cb24cbd76d87a1fd11785061cc7c57d3"}, + {file = "jellyfish-1.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cad69bc4174757abfc232c2741e3a6e08ad2050be9d84d91f58ff0936a8cce40"}, + {file = "jellyfish-1.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0271686a4e0cfa67a62ae151e71ef556df1b562373a29d107c3bff4f2308d3ec"}, + {file = "jellyfish-1.0.3-cp312-none-win32.whl", hash = "sha256:9af1447ebff6e8429ccd09ae632becb5612a0ea6bd9eb3aa8c2069a5586b0455"}, + {file = "jellyfish-1.0.3-cp312-none-win_amd64.whl", hash = "sha256:c5aa3b4775c9eb7c814eb0eab5632b5ff476fdfea2be83b10d40bb821e01aa05"}, + {file = "jellyfish-1.0.3-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:b9dc71748365e9de24cca05920d17ef709a31a37c791105e049e37cfbccac3f5"}, + {file = "jellyfish-1.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f56a005e0bbbb45e465904db014e76020edc12ebe042bf5b4705089e14a07480"}, + {file = "jellyfish-1.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7432c1d5c37563f913537716473b59466d2e8494aaf15bcd80f1778aa87365f2"}, + {file = "jellyfish-1.0.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb10db8375fec009fd5880de8158d6c4e43f09333297e53301a4579f526b39a6"}, + {file = "jellyfish-1.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8909ada3ca12e327b10e46e63b4db7d59a2ebea489dc6d1f7f7569fc89a728cf"}, + {file = "jellyfish-1.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:488f7174aeb759c92c9deadf393f81b38178f9397690d8692fd319fe943b313f"}, + {file = "jellyfish-1.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6e827f40d976a04502c21d20f8b788ef585f8a04a3c1b8b9b896492b463c5f94"}, + {file = "jellyfish-1.0.3-cp38-none-win32.whl", hash = "sha256:70afd781d99e04e9749991a2e5f8c78a0b2fc28076b9e30d7ff71ac421327747"}, + {file = "jellyfish-1.0.3-cp38-none-win_amd64.whl", hash = "sha256:bbc5e8285bc38b10fba020154f0ba4b0a7af8e5bd95d28a48ff9bedca0a32bbe"}, + {file = "jellyfish-1.0.3-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:1472d4dc90b9af65c7ebeea846535f507c693411bacfd5ec954ea65454211e19"}, + {file = "jellyfish-1.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:36e5b76447f91f08af1eda5c880aec00533e49517e206ea1b856e956f2a721bc"}, + {file = "jellyfish-1.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab07dedd2d30af9da37c1b80c0a44bde8e2dbff240064beaf33bb6d6d6b3228"}, + {file = "jellyfish-1.0.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4351398fee07578d232625deb8fdadf7949c322e68ffd9e50b374ff0429d4071"}, + {file = "jellyfish-1.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004f6ab661ae4c591be1aed604fbf5976415f1bd771274d29ecdd59b82ef0518"}, + {file = "jellyfish-1.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:45a20a9ab3bfbc7983f60548aa5aff02408bb64de959a765636f8e1b5167266a"}, + {file = "jellyfish-1.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:024d58f45d24565a0ccd129a1a90bb85f9e3fa4ddd4f50dbbd3ac39feac0bb7b"}, + {file = "jellyfish-1.0.3-cp39-none-win32.whl", hash = "sha256:c59f5cdc04b62f8f4ff4575dcda66c3def012ede0049d8e199981cd155cb74c5"}, + {file = "jellyfish-1.0.3-cp39-none-win_amd64.whl", hash = "sha256:1c3be01748738812d4674eb6badcd7e42a885700a4a50c8872683bd3687b58ec"}, + {file = "jellyfish-1.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f581d9b5b9e4f1967b980a31c584f01e62fc6ab24f94834a55d343bd438ccee"}, + {file = "jellyfish-1.0.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11f98c3041dbc80229916809b7a4aa9cc27142f385b6f334d9d8215b217fe87a"}, + {file = "jellyfish-1.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92e17def9f6a3f6d6e7bc49040907a969ab6c1acd5bbc5db92fd65662f509ee4"}, + {file = "jellyfish-1.0.3-pp310-pypy310_pp73-musllinux_1_1_i686.whl", hash = "sha256:5b957602492ae611e052bc945693b91e9ab2bf1c11273c731f5ac718721d6e6e"}, + {file = "jellyfish-1.0.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:a2ce180fd087425e9a6f94979c84657cd91f331c3811b146c83605465d78130c"}, + {file = "jellyfish-1.0.3-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:141665836bb227e6c7e16195b292c8e0624ad6ba48471984c7d5a593876ba16b"}, + {file = "jellyfish-1.0.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:48dd2b6165dae690d051a9b39f92cf37f8e583c7ac64c83718d07ba7dc68a7f8"}, + {file = "jellyfish-1.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e164002d9171a3c1dc6e256a6ab7c3c61f9e0dcb5a1eec77846e664cc1b10e6d"}, + {file = "jellyfish-1.0.3-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:80b0330071a52a33a1d816f7058798196d613916258565ee5f1cf9d1165c95bc"}, + {file = "jellyfish-1.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ea8a7fe23f7794be55e36aac538c37ddb9cd21282779ac21afb06b280d6a96"}, + {file = "jellyfish-1.0.3-pp38-pypy38_pp73-musllinux_1_1_i686.whl", hash = "sha256:70d9bf2fec251dcfa96443ec6710e845ebdeabf254252090c2614843a56753dd"}, + {file = "jellyfish-1.0.3-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:9ffdec3137b4fbeadcf3ba49b75139e3271bd0fb3fa947ebb39c9d5a3af1db9e"}, + {file = "jellyfish-1.0.3-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:bbfd54e277ee4c72c383b9feeb8e1596466a17299508864892d76ba6a41a8d03"}, + {file = "jellyfish-1.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8b25acc9df837be60f72841a3c658bb74fdacb108631d67cb4c92eebf6bb2729"}, + {file = "jellyfish-1.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335f9798c5e7d03d1af748347d69cda740b71baf23a7f03275bf7b8b13d87fba"}, + {file = "jellyfish-1.0.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fafbc56ceaf88563aaf40b00c90b3a75d2d115a01082170091cab77fb14ca8d5"}, + {file = "jellyfish-1.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8299663d497c0357b92023cf457b485d43d2e969da287f345ab128d145e9f73d"}, + {file = "jellyfish-1.0.3-pp39-pypy39_pp73-musllinux_1_1_i686.whl", hash = "sha256:9a52bbd1d71f6de4fd0f24e2673deffcd93289eea6b973887ebd5d45edc865c6"}, + {file = "jellyfish-1.0.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d606654953309a37b2f0ceaa3bfb26eeecc50d05577a037ac1f55f96f963e4a2"}, + {file = "jellyfish-1.0.3.tar.gz", hash = "sha256:ddb22b7155f208e088352283ee78cb4ef2d2067a76e148a8bb43d177f32b37d2"}, ] [[package]] name = "markdown-it-py" -version = "2.2.0" +version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" -typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] @@ -610,14 +566,13 @@ compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0 linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -629,7 +584,6 @@ files = [ name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -641,7 +595,6 @@ files = [ name = "mediafile" version = "0.12.0" description = "Handles low-level interfacing for files' tags. Wraps Mutagen to" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -658,21 +611,19 @@ test = ["tox"] [[package]] name = "multimethod" -version = "1.9.1" +version = "1.10" description = "Multiple argument dispatching." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "multimethod-1.9.1-py3-none-any.whl", hash = "sha256:52f8f1f2b9d5a4c7adfdcc114dbeeebe3245a4420801e8807e26522a79fb6bc2"}, - {file = "multimethod-1.9.1.tar.gz", hash = "sha256:1589bf52ca294667fd15527ea830127c763f5bfc38562e3642591ffd0fd9d56f"}, + {file = "multimethod-1.10-py3-none-any.whl", hash = "sha256:afd84da9c3d0445c84f827e4d63ad42d17c6d29b122427c6dee9032ac2d2a0d4"}, + {file = "multimethod-1.10.tar.gz", hash = "sha256:daa45af3fe257f73abb69673fd54ddeaf31df0eb7363ad6e1251b7c9b192d8c5"}, ] [[package]] name = "munkres" version = "1.1.4" description = "Munkres (Hungarian) algorithm for the Assignment Problem" -category = "main" optional = false python-versions = "*" files = [ @@ -684,7 +635,6 @@ files = [ name = "musicbrainzngs" version = "0.7.1" description = "Python bindings for the MusicBrainz NGS and the Cover Art Archive webservices" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -694,69 +644,66 @@ files = [ [[package]] name = "mutagen" -version = "1.46.0" +version = "1.47.0" description = "read and write audio tags for many formats" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "mutagen-1.46.0-py3-none-any.whl", hash = "sha256:8af0728aa2d5c3ee5a727e28d0627966641fddfe804c23eabb5926a4d770aed5"}, - {file = "mutagen-1.46.0.tar.gz", hash = "sha256:6e5f8ba84836b99fe60be5fb27f84be4ad919bbb6b49caa6ae81e70584b55e58"}, + {file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"}, + {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, ] [[package]] name = "mypy" -version = "1.4.1" +version = "1.10.0" description = "Optional static typing for Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, - {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, - {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, - {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, - {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, - {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, - {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, - {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, - {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, - {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, - {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, - {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, - {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, - {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, - {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, - {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, - {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, - {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, - {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, - {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, - {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, - {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, - {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, - {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, - {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, - {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -768,7 +715,6 @@ files = [ name = "ordered-set" version = "4.1.0" description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -781,50 +727,42 @@ dev = ["black", "mypy", "pytest"] [[package]] name = "packaging" -version = "23.1" +version = "24.0" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] name = "platformdirs" -version = "3.10.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" +version = "4.2.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, + {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} - [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.2.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -833,7 +771,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pycodestyle" version = "2.9.1" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -843,23 +780,22 @@ files = [ [[package]] name = "pycountry" -version = "22.3.5" +version = "23.12.11" description = "ISO country, subdivision, language, currency and script definitions and their translations" -category = "main" optional = false -python-versions = ">=3.6, <4" +python-versions = ">=3.8" files = [ - {file = "pycountry-22.3.5.tar.gz", hash = "sha256:b2163a246c585894d808f18783e19137cb70a0c18fb36748dc01fc6f109c1646"}, + {file = "pycountry-23.12.11-py3-none-any.whl", hash = "sha256:2ff91cff4f40ff61086e773d61e72005fe95de4a57bfc765509db05695dc50ab"}, + {file = "pycountry-23.12.11.tar.gz", hash = "sha256:00569d82eaefbc6a490a311bfa84a9c571cff9ddbf8b0a4f4e7b4f868b4ad925"}, ] [package.dependencies] -setuptools = "*" +importlib-resources = {version = ">=5.12.0,<6.0.0", markers = "python_version < \"3.9\""} [[package]] name = "pyflakes" version = "2.5.0" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -869,78 +805,80 @@ files = [ [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "2.13.9" +version = "3.1.0" description = "python code static checker" -category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.8.0" files = [ - {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"}, - {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"}, + {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, + {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, ] [package.dependencies] -astroid = ">=2.11.5,<=2.12.0-dev0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -dill = ">=0.2" -isort = ">=4.2.5,<6" +astroid = ">=3.1.0,<=3.2.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, +] +isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] -testutil = ["gitpython (>3)"] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] [[package]] name = "pytest" -version = "7.4.0" +version = "8.2.0" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] @@ -948,44 +886,47 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] -name = "pytest-lazy-fixture" -version = "0.6.3" -description = "It helps to use fixtures in pytest.mark.parametrize" -category = "dev" +name = "pytest-randomly" +version = "3.15.0" +description = "Pytest plugin to randomly order tests and control random.seed." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "pytest-lazy-fixture-0.6.3.tar.gz", hash = "sha256:0e7d0c7f74ba33e6e80905e9bfd81f9d15ef9a790de97993e34213deb5ad10ac"}, - {file = "pytest_lazy_fixture-0.6.3-py3-none-any.whl", hash = "sha256:e0b379f38299ff27a653f03eaa69b08a6fd4484e46fd1c9907d984b9f9daeda6"}, + {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"}, + {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"}, ] [package.dependencies] -pytest = ">=3.2.5" +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +pytest = "*" [[package]] -name = "pytest-randomly" -version = "3.12.0" -description = "Pytest plugin to randomly order tests and control random.seed." -category = "dev" +name = "pytest-xdist" +version = "3.5.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"}, - {file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"}, + {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, + {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, ] [package.dependencies] -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -pytest = "*" +execnet = ">=1.1" +pytest = ">=6.2.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] [[package]] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -994,6 +935,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1001,8 +943,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1019,6 +969,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1026,6 +977,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1035,7 +987,6 @@ files = [ name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1053,27 +1004,15 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "rgbxy" -version = "0.5" -description = "RGB conversion tool written in Python for Philips Hue." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "rgbxy-0.5.tar.gz", hash = "sha256:78a099f5f8d30c82e95f1941850bc49e42649d6beac8f96e3a301e97437d6d30"}, -] - [[package]] name = "rich" -version = "13.5.2" +version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "dev" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, - {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] @@ -1086,43 +1025,27 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rich-tables" -version = "0.1.2" +version = "0.3.0" description = "Ready-made rich tables for various purposes" -category = "dev" optional = false -python-versions = ">=3.6.3,<4" +python-versions = ">=3.7,<4" files = [ - {file = "rich-tables-0.1.2.tar.gz", hash = "sha256:03f1041c7b1e0a8bca5e989dd63dd29bab8ee1759bfd2ec6c080a464da36c7a6"}, - {file = "rich_tables-0.1.2-py3-none-any.whl", hash = "sha256:b937968680b2e85dc107bb4726ee164f35241badaf461716f3ec1bb4396ad45c"}, + {file = "rich_tables-0.3.0-py3-none-any.whl", hash = "sha256:364bbb1e8da7166aac675dfaad082be4b902534de9e1a35b39a1a907e3c62998"}, + {file = "rich_tables-0.3.0.tar.gz", hash = "sha256:0dc5f08c82565fc3f59b29aac4fb49d727d2c062a90057f09d52ed23f3baafde"}, ] [package.dependencies] multimethod = "*" -rgbxy = ">=0.5" rich = ">=12.3.0" -[[package]] -name = "setuptools" -version = "68.0.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, -] - [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +hue = ["rgbxy (>=0.5)"] +sql = ["sqlparse (>=0.4.4,<0.5.0)"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1134,7 +1057,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1143,251 +1065,85 @@ files = [ ] [[package]] -name = "typed-ast" -version = "1.5.5" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" +name = "tomlkit" +version = "0.12.4" +description = "Style preserving TOML library" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, - {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, - {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, - {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, - {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, - {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, - {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, - {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, - {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, - {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, - {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, - {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, - {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, - {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, - {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, - {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, - {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, - {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, - {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, - {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, - {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, - {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, - {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, - {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, - {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, - {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, - {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, - {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, - {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, - {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, - {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, - {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, - {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, - {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, - {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, - {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, - {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, - {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, - {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, - {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, - {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, ] [[package]] name = "types-requests" -version = "2.31.0.2" +version = "2.31.0.20240406" description = "Typing stubs for requests" -category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.2.tar.gz", hash = "sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40"}, - {file = "types_requests-2.31.0.2-py3-none-any.whl", hash = "sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a"}, + {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, + {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, ] [package.dependencies] -types-urllib3 = "*" - -[[package]] -name = "types-setuptools" -version = "68.0.0.3" -description = "Typing stubs for setuptools" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "types-setuptools-68.0.0.3.tar.gz", hash = "sha256:d57ae6076100b5704b3cc869fdefc671e1baf4c2cd6643f84265dfc0b955bf05"}, - {file = "types_setuptools-68.0.0.3-py3-none-any.whl", hash = "sha256:fec09e5c18264c5c09351c00be01a34456fb7a88e457abe97401325f84ad9d36"}, -] - -[[package]] -name = "types-six" -version = "1.16.21.9" -description = "Typing stubs for six" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "types-six-1.16.21.9.tar.gz", hash = "sha256:746e6c25b8c48b3c8ab9efe7f68022839111de423d35ba4b206b88b12d75f233"}, - {file = "types_six-1.16.21.9-py3-none-any.whl", hash = "sha256:1591a09430a3035326da5fdb71692d0b3cc36b25a440cc5929ca6241f3984705"}, -] - -[[package]] -name = "types-urllib3" -version = "1.26.25.14" -description = "Typing stubs for urllib3" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, - {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, -] +urllib3 = ">=2" [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] name = "unidecode" -version = "1.3.6" +version = "1.3.8" description = "ASCII transliterations of Unicode text" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, - {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, + {file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"}, + {file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"}, ] [[package]] name = "urllib3" -version = "2.0.4" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "wrapt" -version = "1.15.0" -description = "Module for decorators, wrappers and monkey patching." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, -] - [[package]] name = "zipp" -version = "3.15.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" -python-versions = ">=3.7, <4" -content-hash = "61308512a07321866f15145f41fbd2206b48b552234019e447eb691c078c599b" +python-versions = ">=3.8, <4" +content-hash = "2fe06977a51a2fce77e2c39e43a9932d791a4a492290942122bcbd366048e083" diff --git a/pyproject.toml b/pyproject.toml index 0502787..a74f4b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "beetcamp" -version = "0.17.2" +version = "0.18.0" description = "Bandcamp autotagger source for beets (http://beets.io)." authors = ["Šarūnas Nejus "] readme = "README.md" @@ -25,30 +25,27 @@ Changelog = "https://github.com/snejus/beetcamp/blob/master/CHANGELOG.md" "Bug Tracker" = "https://github.com/snejus/beetcamp/issues" [tool.poetry.dependencies] -python = ">=3.7, <4" +python = ">=3.8, <4" requests = ">=2.27" -cached-property = { version = ">=1.5.2", python = "<3.8" } pycountry = ">=20.7.3" -beets = ">=1.4" +beets = ">=1.4,<=1.6.0" ordered-set = ">=4.0" +packaging = ">=24.0" [tool.poetry.dev-dependencies] -dataclasses = { version = ">=0.7", python = "<3.7" } flake8 = ">=3.8.4" -flake8-bugbear = "^22.7.1" -flake8-comprehensions = "^3.10.0" -flake8-eradicate = "^1.3.0" +flake8-bugbear = ">=22.7.1" +flake8-comprehensions = ">=3.10.0" +flake8-eradicate = ">=1.3.0" mypy = ">=0.790" pylint = ">=2.7.4" pytest = ">=6.2" pytest-cov = ">=2.10.1" pytest-randomly = ">=3.10" -pytest-lazy-fixture = ">=0.6.3" +pytest-xdist = ">=3.5.0" rich-tables = "*" -types-setuptools = ">=57.0.0" types-requests = ">=2.25.0" -types-six = ">=0.1.7" [tool.poetry.scripts] beetcamp = "beetsplug.bandcamp:main" diff --git a/setup.cfg b/setup.cfg index 7aec6cc..50f76a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ addopts = -vv -k "not lib" --no-header - --junit-xml=.reports/pytest-tests.xml + --junit-xml=.reports/test-report.xml --code-highlight=no --strict-config --tb=short @@ -15,6 +15,8 @@ addopts = --cov-report=xml:.reports/coverage.xml --cov-report=html:.reports/html --cov-branch + --cov-append + --cov-context=test markers = need_connection: end-to-end tests that require internet connection @@ -27,7 +29,6 @@ testpaths = tests [coverage:run] -dynamic_context = test_function data_file = .reports/coverage/data branch = true relative_files = true @@ -57,6 +58,7 @@ max-complexity = 7 [mypy] files = beetsplug/bandcamp +exclude = test_*|migrations explicit_package_bases = true strict = true warn_unreachable = true @@ -67,13 +69,12 @@ namespace_packages = true show_error_codes = true show_column_numbers = true allow_subclassing_any = true +allow_untyped_decorators = true +allow_untyped_calls = true [mypy-beets.*] ignore_missing_imports = true -[mypy-cached_property] -ignore_missing_imports = true - [mypy-pycountry] ignore_missing_imports = true diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 9e5b6f2..0000000 --- a/sonar-project.properties +++ /dev/null @@ -1,13 +0,0 @@ -sonar.branch.target=main -sonar.core.codeCoveragePlugin=cobertura -sonar.organization=snejus -sonar.projectKey=snejus_beetcamp -sonar.pullrequest.github.summary_comment=false -sonar.python.version=3.8 -sonar.python.coverage.reportPaths=.reports/coverage.xml -sonar.python.flake8.reportPaths=flake.log -sonar.python.pylint.reportPaths=pylint.log -sonar.python.xunit.reportPath=.reports/pytest-tests.xml -sonar.sourceEncoding=UTF-8 -sonar.sources=beetsplug -sonar.tests=tests diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index a4f0d8f..ca2f2b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Pytest fixtures for tests.""" + import json import os import re @@ -10,6 +11,7 @@ import pytest from beets.autotag.hooks import AlbumInfo, TrackInfo from beetsplug.bandcamp import DEFAULT_CONFIG +from beetsplug.bandcamp.metaguru import ALBUMTYPES_LIST_SUPPORT from rich.console import Console @@ -38,7 +40,7 @@ def pytest_addoption(parser): def pytest_terminal_summary(terminalreporter, exitstatus, config): base = config.getoption("base") target = config.getoption("target") - terminalreporter.write(f"---\nCompared {target} against {base}\n---") + terminalreporter.write(f"--- Compared {target} against {base} ---\n") @pytest.fixture(scope="session") @@ -130,6 +132,9 @@ def release(request): expected_output = json.load(out_f) if isinstance(expected_output, dict): expected_output = [expected_output] + if ALBUMTYPES_LIST_SUPPORT: + for release in expected_output: + release["albumtypes"] = release["albumtypes"].split("; ") return input_json, expected_output diff --git a/tests/json/album.json b/tests/json/album.json index 9516e6a..ac02276 100644 --- a/tests/json/album.json +++ b/tests/json/album.json @@ -122,10 +122,10 @@ } ], "availability": "SoldOut", - "price": 12, + "price": 12.0, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 12 + "minPrice": 12.0 }, "url": "https://ute-rec.bandcamp.com/album/ute004#p122109000-buy" } @@ -154,7 +154,7 @@ }, "copyrightNotice": "All Rights Reserved", "creditText": "Production: Mikkel Haraldstad\r\nMaster: Joel Krozer, 6bitdeep\r\nGraphics: Haider Ahmed", - "dateModified": "22 Feb 2021 10:59:51 GMT", + "dateModified": "19 Feb 2023 14:55:18 GMT", "datePublished": "17 Jul 2020 00:00:00 GMT", "description": "Our dear Mikkel serving four emotional and warm trancers focused on the journey within.", "image": "https://f4.bcbits.com/img/a1035657740_10.jpg", @@ -199,7 +199,7 @@ "value": 2048 } ], - "description": "✣ Label\n✣ Club nights\n✣ Forest raves", + "description": "✣ Record label\n✣ Club nights\n✣ Forest gatherings", "foundingLocation": { "@type": "Place", "name": "Oslo, Norway" diff --git a/tests/json/album_in_titles.json b/tests/json/album_in_titles.json index eaada1e..8747fc1 100644 --- a/tests/json/album_in_titles.json +++ b/tests/json/album_in_titles.json @@ -61,7 +61,7 @@ "name": "GutterFunk: All Subject To Vibes [Various Artists LP]" }, { - "@id": "https://gutterfunkuk.bandcamp.com/album/gutterfunk-all-subject-to-vibes-various-artists-lp#b46313789", + "@id": "https://gutterfunkuk.bandcamp.com/album/gutterfunk-all-subject-to-vibes-various-artists-lp#b56755260", "@type": [ "MusicRelease", "Product" @@ -70,7 +70,7 @@ { "@type": "PropertyValue", "name": "item_id", - "value": 46313789 + "value": 56755260 }, { "@type": "PropertyValue", @@ -90,7 +90,7 @@ { "@type": "PropertyValue", "name": "art_id", - "value": 4103233153 + "value": 4294573575 }, { "@type": "PropertyValue", @@ -99,17 +99,17 @@ } ], "image": [ - "https://f4.bcbits.com/img/a4103233153_10.jpg" + "https://f4.bcbits.com/img/a4294573575_10.jpg" ], "musicReleaseFormat": "DigitalFormat", - "name": "full digital discography (44 releases)", + "name": "full digital discography (49 releases)", "offers": { "@type": "Offer", "additionalProperty": [ { "@type": "PropertyValue", "name": "bundle_size", - "value": 44 + "value": 49 }, { "@type": "PropertyValue", @@ -118,12 +118,12 @@ } ], "availability": "OnlineOnly", - "price": 92.3, + "price": 102.05, "priceCurrency": "GBP", "priceSpecification": { - "minPrice": 92.3 + "minPrice": 102.05 }, - "url": "https://gutterfunkuk.bandcamp.com/album/gutterfunk-all-subject-to-vibes-various-artists-lp#b46313789-buy" + "url": "https://gutterfunkuk.bandcamp.com/album/gutterfunk-all-subject-to-vibes-various-artists-lp#b56755260-buy" } }, { @@ -237,7 +237,7 @@ "@type": "Place", "name": "Bristol, UK" }, - "genre": "https://bandcamp.com/tag/electronic", + "genre": "https://bandcamp.com/discover/electronic", "image": "https://f4.bcbits.com/img/0010127893_10.jpg", "mainEntityOfPage": [ { diff --git a/tests/json/album_with_track_alt.json b/tests/json/album_with_track_alt.json index 984d953..44064cd 100644 --- a/tests/json/album_with_track_alt.json +++ b/tests/json/album_with_track_alt.json @@ -132,10 +132,10 @@ } ], "availability": "SoldOut", - "price": 12, + "price": 12.0, "priceCurrency": "GBP", "priceSpecification": { - "minPrice": 12 + "minPrice": 12.0 }, "url": "https://foldrecords.bandcamp.com/album/fld001-gareth-wild-common-assault-ep#p2171282163-buy" } diff --git a/tests/json/artist_catalognum.json b/tests/json/artist_catalognum.json index 86e534b..91238ad 100644 --- a/tests/json/artist_catalognum.json +++ b/tests/json/artist_catalognum.json @@ -61,7 +61,7 @@ "name": "PROCESS 404 'Cybernetic Punk Unit'" }, { - "@id": "https://blvckplvgue.bandcamp.com/album/process-404-cybernetic-punk-unit#b51380968", + "@id": "https://blvckplvgue.bandcamp.com/album/process-404-cybernetic-punk-unit#b54684199", "@type": [ "MusicRelease", "Product" @@ -70,7 +70,7 @@ { "@type": "PropertyValue", "name": "item_id", - "value": 51380968 + "value": 54684199 }, { "@type": "PropertyValue", @@ -90,7 +90,7 @@ { "@type": "PropertyValue", "name": "art_id", - "value": 219819829 + "value": 257256293 }, { "@type": "PropertyValue", @@ -99,17 +99,17 @@ } ], "image": [ - "https://f4.bcbits.com/img/a0219819829_10.jpg" + "https://f4.bcbits.com/img/a0257256293_10.jpg" ], "musicReleaseFormat": "DigitalFormat", - "name": "full digital discography (28 releases)", + "name": "full digital discography (29 releases)", "offers": { "@type": "Offer", "additionalProperty": [ { "@type": "PropertyValue", "name": "bundle_size", - "value": 28 + "value": 29 }, { "@type": "PropertyValue", @@ -118,12 +118,12 @@ } ], "availability": "OnlineOnly", - "price": 88.08, + "price": 91.08, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 88.08 + "minPrice": 91.08 }, - "url": "https://blvckplvgue.bandcamp.com/album/process-404-cybernetic-punk-unit#b51380968-buy" + "url": "https://blvckplvgue.bandcamp.com/album/process-404-cybernetic-punk-unit#b54684199-buy" } }, { @@ -204,7 +204,7 @@ "@type": "Place", "name": "Paris, France" }, - "genre": "https://bandcamp.com/tag/electronic", + "genre": "https://bandcamp.com/discover/electronic", "image": "https://f4.bcbits.com/img/0012129458_10.jpg", "mainEntityOfPage": [ { diff --git a/tests/json/artist_mess.json b/tests/json/artist_mess.json index b442df1..89a0cea 100644 --- a/tests/json/artist_mess.json +++ b/tests/json/artist_mess.json @@ -175,12 +175,12 @@ "value": 1920 } ], - "description": "a maverick electronic music producer, since 2000 Psykovsky has worked with a number of different solo and collaborative projects, and is nowadays acknowledged as one of the most auspicious psytrance artists - check out Psykovsky's alternative digital audio-visual releases at https://opensea.io/VasilyPsykovsky?tab=created and https://www.youtube.com/channel/UCu1BnVhnFn0pcVZnkwmOAZQ", + "description": "a maverick electronic music producer, since 2000 Psykovsky has worked with a number of different solo and collaborative projects, and is nowadays acknowledged as one of the most auspicious psytrance artists - check out Psykovsky's alternative digital audio-visual releases at https://www.youtube.com/channel/UCu1BnVhnFn0pcVZnkwmOAZQ", "foundingLocation": { "@type": "Place", "name": "Niue" }, - "genre": "https://bandcamp.com/tag/electronic", + "genre": "https://bandcamp.com/discover/electronic", "image": "https://f4.bcbits.com/img/0021724693_10.jpg", "name": "Psykovsky", "subjectOf": [ diff --git a/tests/json/description_meta.json b/tests/json/description_meta.json index bf92ff3..13dab72 100644 --- a/tests/json/description_meta.json +++ b/tests/json/description_meta.json @@ -119,7 +119,7 @@ } }, { - "@id": "https://diffusereality.bandcamp.com/album/francois-dillinger-icosahedrone-lp#b51811323", + "@id": "https://diffusereality.bandcamp.com/album/francois-dillinger-icosahedrone-lp#b59852101", "@type": [ "MusicRelease", "Product" @@ -128,7 +128,7 @@ { "@type": "PropertyValue", "name": "item_id", - "value": 51811323 + "value": 59852101 }, { "@type": "PropertyValue", @@ -148,7 +148,7 @@ { "@type": "PropertyValue", "name": "art_id", - "value": 468202331 + "value": 1620809228 }, { "@type": "PropertyValue", @@ -157,31 +157,31 @@ } ], "image": [ - "https://f4.bcbits.com/img/a0468202331_10.jpg" + "https://f4.bcbits.com/img/a1620809228_10.jpg" ], "musicReleaseFormat": "DigitalFormat", - "name": "full digital discography (729 releases)", + "name": "full digital discography (1014 releases)", "offers": { "@type": "Offer", "additionalProperty": [ { "@type": "PropertyValue", "name": "bundle_size", - "value": 729 + "value": 1014 }, { "@type": "PropertyValue", "name": "discount", - "value": 0.85 + "value": 0.9 } ], "availability": "OnlineOnly", - "price": 850.21, + "price": 772.5, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 850.21 + "minPrice": 772.5 }, - "url": "https://diffusereality.bandcamp.com/album/francois-dillinger-icosahedrone-lp#b51811323-buy" + "url": "https://diffusereality.bandcamp.com/album/francois-dillinger-icosahedrone-lp#b59852101-buy" } }, { @@ -270,30 +270,14 @@ "@type": "PropertyValue", "name": "has_policies", "value": true - }, - { - "@type": "PropertyValue", - "name": "image_height", - "value": 2917 - }, - { - "@type": "PropertyValue", - "name": "image_id", - "value": 29960568 - }, - { - "@type": "PropertyValue", - "name": "image_width", - "value": 2917 } ], - "description": "Independent Record Label founded in 2013.\nArts Festival: Teorema\nSublabel: Periphylla\n۩ Send a private message to visit our Record shop in Barcelona 💌\n→ https://linktr.ee/diffusereality", + "description": "Independent Record Label founded in 2013.\nArts Festival: Teorema\nSublabel: Periphylla\nSublabel: APS : A Psychomagic Story\nhttps://linktr.ee/diffusereality", "foundingLocation": { "@type": "Place", "name": "Portugal" }, - "genre": "https://bandcamp.com/tag/electronic", - "image": "https://f4.bcbits.com/img/0029960568_10.jpg", + "genre": "https://bandcamp.com/discover/electronic", "mainEntityOfPage": [ { "@type": "WebPage", diff --git a/tests/json/edge_cases.json b/tests/json/edge_cases.json index 7eda87a..6f755ae 100644 --- a/tests/json/edge_cases.json +++ b/tests/json/edge_cases.json @@ -61,7 +61,7 @@ "name": "NYH244 Less Weird Parallel Universe - Nearly 20 Years of Erikoisdance" }, { - "@id": "https://newyorkhaunted.bandcamp.com/album/nyh244-less-weird-parallel-universe-nearly-20-years-of-erikoisdance#b51689474", + "@id": "https://newyorkhaunted.bandcamp.com/album/nyh244-less-weird-parallel-universe-nearly-20-years-of-erikoisdance#b59859656", "@type": [ "MusicRelease", "Product" @@ -70,7 +70,7 @@ { "@type": "PropertyValue", "name": "item_id", - "value": 51689474 + "value": 59859656 }, { "@type": "PropertyValue", @@ -90,7 +90,7 @@ { "@type": "PropertyValue", "name": "art_id", - "value": 231811252 + "value": 4281703832 }, { "@type": "PropertyValue", @@ -99,17 +99,17 @@ } ], "image": [ - "https://f4.bcbits.com/img/a0231811252_10.jpg" + "https://f4.bcbits.com/img/a4281703832_10.jpg" ], "musicReleaseFormat": "DigitalFormat", - "name": "full digital discography (342 releases)", + "name": "full digital discography (383 releases)", "offers": { "@type": "Offer", "additionalProperty": [ { "@type": "PropertyValue", "name": "bundle_size", - "value": 342 + "value": 383 }, { "@type": "PropertyValue", @@ -118,12 +118,12 @@ } ], "availability": "OnlineOnly", - "price": 135.72, + "price": 183.07, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 135.72 + "minPrice": 183.07 }, - "url": "https://newyorkhaunted.bandcamp.com/album/nyh244-less-weird-parallel-universe-nearly-20-years-of-erikoisdance#b51689474-buy" + "url": "https://newyorkhaunted.bandcamp.com/album/nyh244-less-weird-parallel-universe-nearly-20-years-of-erikoisdance#b59859656-buy" } }, { @@ -207,7 +207,7 @@ }, "copyrightNotice": "All Rights Reserved", "creditText": "Less Weird Parallel Universe\r\nNearly 20 Years of Erikoisdance\r\nAll tracks previously released on CDR.\r\ncompiled by Tom & Vince\r\n\r\n1) Track 3 \r\nMallisto\r\nLive at Mannerheimintie 100 B 48, 31.12.1998 (Erikoisdance 19, 2014)\r\n\r\n2) Music for Dealers \r\nTwisted Krister\r\nRingbahn (Erikoisdance 14, 2010)\r\n\r\n3) _ _ _ \r\nErikoismies\r\nSIMO Tracks (Erikoisdance 15, 2012)\r\n\r\n4) Mind Freak\r\nChris Angel\r\nDiabolus in Musica (Erikoisdance 8, 2008)\r\n\r\n5) New Age Home Recording I \r\nMr. Yakamoto\r\nNew Age Home Recordings (Erikoisdance 10, 2008)\r\n\r\n6) Puujumala \r\nNon-baryonic Form\r\nStereo Balance in The Phythm Part (Erikoisdance 7, 2008)\r\n\r\n7) Flogiston (ODJ Harri Keys of Life House Music Edit) \r\nOmni Gideon (edit by ODJ Harri)\r\nSähkölasku (Erikoisdance 11, 2008)\r\n\r\n8) Schweinfurt Green\r\nPoly-T\r\nComplete Works (Erikoisdance 6, 2007)\r\n \r\n9) Laulu numero 1\r\nMallisto\r\nJuhannus 3008 (Erikoisdance 13, 2009)\r\n\r\n10) Nitrogen Glaciers\r\nNon-baryonic Form\r\nSpatial Guest (Erikoisdance 20, 2016)\r\n\r\n11) CDR Track VI\r\nErikoismies\r\nCDR Tracks (Erikoisdance 9, 2008)\r\n\r\n12) Moskstraumen\r\nDie Todesmaschine \r\nMoskstraumen (Erikoisdance 12, 2009)\r\n\r\n13) MN-50\r\nSauce & Cop\r\nJonkun on ymmärrettävä mitä tehdään (Erikoisdance 16, 2013)\r\n\r\n14) TSIMPLNO\r\nTreepio\r\nTransistor Amnesia (Erikoisdance 38, 2019)\r\n\r\n15) CDR Track I\r\nErikoismies\r\nCDR Tracks (Erikoisdance 9, 2008)\r\n\r\n16) CB Skull \r\nNon-baryonic Form\r\nStereo Balance in The Phythm Part (Erikoisdance 7, 2008)\r\n\r\n17) Ytterbium 13\r\nOmni Gideon\r\nSähkölasku (Erikoisdance 11, 2008)\r\n\r\n18) Quartz Crisis\r\nSauce & Cop\r\n[Untitled Split CDR] (Erikoisdance 21, 2015)", - "dateModified": "13 Nov 2022 16:08:23 GMT", + "dateModified": "22 Mar 2023 15:37:15 GMT", "datePublished": "03 Sep 2021 00:00:00 GMT", "description": "I met Tom of Eikoismies a good 20 years ago, back when we were both running small labels, focused on electro mostly. We exchanged tracks and stickers and stayed in touch, lost touch and got back in touch. For our followers, it'll hardly be a surprise to hear I am a big fan of his label Erikoisdance, which has been running for 20 years. \r\n\r\nIts a real honor to be able to make a selection of tracks of this CDR-only label available digitally. Especially because they have been releasing really top notch experimental electronics and techno in a very limited way. The old-school way, of burning cdr's and hand drawing on them and sending them to fans all over the world. This has kept them very autonomous in their releases and style of music. But man.. it's just too good to not share with the rest of the world via ze webbzz.\r\n\r\nI hope you enjoy this selection and I hope you check out Erikoisdance and order one of their timeless. kvlt, trve techno artifacts.\r\n\r\nKeep polycarbonate alive!\r\nhttp://mustakirahvi.net/erikoisdance/", "image": "https://f4.bcbits.com/img/a2614409477_10.jpg", @@ -266,7 +266,7 @@ "@type": "Place", "name": "Tilburg, Netherlands" }, - "genre": "https://bandcamp.com/tag/electronic", + "genre": "https://bandcamp.com/discover/electronic", "image": "https://f4.bcbits.com/img/0005400029_10.jpg", "name": "New York Haunted", "subjectOf": [ diff --git a/tests/json/ep.json b/tests/json/ep.json index b747118..b456326 100644 --- a/tests/json/ep.json +++ b/tests/json/ep.json @@ -123,16 +123,16 @@ } ], "availability": "InStock", - "price": 12, + "price": 9.0, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 12 + "minPrice": 9.0 }, "url": "https://fallingapart.bandcamp.com/album/fa010-kickdown-vienna#p2928220301-buy" } }, { - "@id": "https://fallingapart.bandcamp.com/album/fa010-kickdown-vienna#b50562643", + "@id": "https://fallingapart.bandcamp.com/album/fa010-kickdown-vienna#b58140448", "@type": [ "MusicRelease", "Product" @@ -141,7 +141,7 @@ { "@type": "PropertyValue", "name": "item_id", - "value": 50562643 + "value": 58140448 }, { "@type": "PropertyValue", @@ -161,7 +161,7 @@ { "@type": "PropertyValue", "name": "art_id", - "value": 92683853 + "value": 3598959771 }, { "@type": "PropertyValue", @@ -170,17 +170,17 @@ } ], "image": [ - "https://f4.bcbits.com/img/a0092683853_10.jpg" + "https://f4.bcbits.com/img/a3598959771_10.jpg" ], "musicReleaseFormat": "DigitalFormat", - "name": "full digital discography (18 releases)", + "name": "full digital discography (20 releases)", "offers": { "@type": "Offer", "additionalProperty": [ { "@type": "PropertyValue", "name": "bundle_size", - "value": 18 + "value": 20 }, { "@type": "PropertyValue", @@ -189,12 +189,12 @@ } ], "availability": "OnlineOnly", - "price": 164.8, + "price": 163.2, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 164.8 + "minPrice": 163.2 }, - "url": "https://fallingapart.bandcamp.com/album/fa010-kickdown-vienna#b50562643-buy" + "url": "https://fallingapart.bandcamp.com/album/fa010-kickdown-vienna#b58140448-buy" } }, { @@ -220,11 +220,10 @@ "name": "jeånne, DJ DISRESPECT" }, "copyrightNotice": "All Rights Reserved", - "dateModified": "06 Jul 2022 21:18:27 GMT", + "dateModified": "07 Jul 2023 21:46:09 GMT", "datePublished": "09 Oct 2020 00:00:00 GMT", "image": "https://f4.bcbits.com/img/a3742084072_10.jpg", "keywords": [ - "Ambient", "Chicago", "Electronic", "Experimental", @@ -255,11 +254,6 @@ "name": "has_download_codes", "value": true }, - { - "@type": "PropertyValue", - "name": "has_policies", - "value": true - }, { "@type": "PropertyValue", "name": "image_height", @@ -276,12 +270,12 @@ "value": 480 } ], - "description": "vinyl record label / disc-jockey\n\nbookings:\nfallingapart030@gmail.com", + "description": "DONT BELIEVE THE HYPE!", "foundingLocation": { "@type": "Place", "name": "Frankfurt, Germany" }, - "genre": "https://bandcamp.com/tag/electronic", + "genre": "https://bandcamp.com/discover/electronic", "image": "https://f4.bcbits.com/img/0015313255_10.jpg", "mainEntityOfPage": [ { diff --git a/tests/json/expected/album_with_track_alt.json b/tests/json/expected/album_with_track_alt.json index 9987499..83611ce 100644 --- a/tests/json/expected/album_with_track_alt.json +++ b/tests/json/expected/album_with_track_alt.json @@ -12,7 +12,7 @@ "artist_sort": null, "asin": null, "catalognum": "FLD001", - "comments": "", + "comments": null, "country": "GB", "data_source": "bandcamp", "data_url": "https://foldrecords.bandcamp.com/album/fld001-gareth-wild-common-assault-ep", diff --git a/tests/json/expected/artist_mess.json b/tests/json/expected/artist_mess.json index bfc9290..da2709f 100644 --- a/tests/json/expected/artist_mess.json +++ b/tests/json/expected/artist_mess.json @@ -12,7 +12,7 @@ "artist_sort": null, "asin": null, "catalognum": "", - "comments": "", + "comments": null, "country": "NU", "data_source": "bandcamp", "data_url": "https://psykovsky.bandcamp.com/album/ksolntsu", diff --git a/tests/json/expected/description_meta.json b/tests/json/expected/description_meta.json index fd806d6..0393e32 100644 --- a/tests/json/expected/description_meta.json +++ b/tests/json/expected/description_meta.json @@ -12,7 +12,7 @@ "artist_sort": null, "asin": null, "catalognum": "", - "comments": "", + "comments": null, "country": "PT", "data_source": "bandcamp", "data_url": "https://diffusereality.bandcamp.com/album/francois-dillinger-icosahedrone-lp", diff --git a/tests/json/expected/ep.json b/tests/json/expected/ep.json index 0bb1f51..3137edd 100644 --- a/tests/json/expected/ep.json +++ b/tests/json/expected/ep.json @@ -12,7 +12,7 @@ "artist_sort": null, "asin": null, "catalognum": "fa010", - "comments": "", + "comments": null, "country": "DE", "data_source": "bandcamp", "data_url": "https://fallingapart.bandcamp.com/album/fa010-kickdown-vienna", @@ -20,7 +20,7 @@ "discogs_albumid": null, "discogs_artistid": null, "discogs_labelid": null, - "genre": "ambient, experimental, house, techno", + "genre": "experimental, house, techno", "label": "falling apart", "language": null, "media": "Digital Media", @@ -175,7 +175,7 @@ "discogs_albumid": null, "discogs_artistid": null, "discogs_labelid": null, - "genre": "ambient, experimental, house, techno", + "genre": "experimental, house, techno", "label": "falling apart", "language": null, "media": "Vinyl", diff --git a/tests/json/expected/hex002.json b/tests/json/expected/hex002.json new file mode 100644 index 0000000..f09f4b9 --- /dev/null +++ b/tests/json/expected/hex002.json @@ -0,0 +1,370 @@ +[ + { + "album": "From The Depths", + "album_id": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "tracks": [ + { + "title": "Conflict", + "track_id": "https://hexbarcelona.bandcamp.com/track/conflict", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 328, + "index": 1, + "media": "Digital Media", + "medium": 1, + "medium_index": 1, + "medium_total": 5, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": null, + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Arise", + "track_id": "https://hexbarcelona.bandcamp.com/track/arise", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 298, + "index": 2, + "media": "Digital Media", + "medium": 1, + "medium_index": 2, + "medium_total": 5, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": null, + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Timeless", + "track_id": "https://hexbarcelona.bandcamp.com/track/timeless", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 445, + "index": 3, + "media": "Digital Media", + "medium": 1, + "medium_index": 3, + "medium_total": 5, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": null, + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "END", + "track_id": "https://hexbarcelona.bandcamp.com/track/end", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 312, + "index": 4, + "media": "Digital Media", + "medium": 1, + "medium_index": 4, + "medium_total": 5, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": null, + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Conflict (NX1 Remix)", + "track_id": "https://hexbarcelona.bandcamp.com/track/conflict-nx1-remix", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 331, + "index": 5, + "media": "Digital Media", + "medium": 1, + "medium_index": 5, + "medium_total": 5, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": null, + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + } + ], + "asin": null, + "albumtype": "ep", + "va": false, + "year": 2019, + "month": 10, + "day": 4, + "label": "HEX Recordings - Techno movement", + "mediums": 1, + "artist_sort": null, + "releasegroup_id": null, + "catalognum": "HEX002", + "script": null, + "language": null, + "country": "ES", + "style": "electronic", + "genre": "techno", + "albumstatus": "Official", + "media": "Digital Media", + "albumdisambig": null, + "releasegroupdisambig": null, + "artist_credit": null, + "original_year": null, + "original_month": null, + "original_day": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "discogs_albumid": null, + "discogs_labelid": null, + "discogs_artistid": null, + "albumtypes": "ep", + "comments": "‘Conflict’ opens the release with fast paced percussion, sustained atmospherics and frenzied high hats before ‘Arise’ weaves acid dipped synths into metallic clashes and hard hitting stabs. ’Timeless’ is next, painting haunting textures with stormy effects, leading into ‘END’ which swiftly builds into a powerful cut complete with growling bass. Tying everything together, Nexe Records founders NX1 remix ‘Conflict’, incorporating warped frequencies and a murky low-end.\n\nFeedbacks about the EP:\n► Rebekah - killer industrial vibes here, love all tracks and remix\n► Gary Beck - These are huge, especially the timeless one\n► Dasha Rush - like most of the tracks, thanks\n► Under Black Helmet - The whole EP is great!\n► Randomer - great tracks\n► Remco Beekwilder - Conflict is a belter, thanks!!\n► Raffaele Attanasio - I'll play for sure!!!\n► Henning Baer - TOP!\n► Stephanie Sykes - Yaaaaaasssss.... loving this release!! <3\n► Insolate - Arise super cool!\n► Joe Farr - NX1 remix is great. I also like Timeless a lot. Thanks.\n► Nur Jaber - VII <3\n► Cleric - Nice EP will play thanks\n\nand many more...\n\nMastered at The Bass Valley Studios by Alan Lockwood" + }, + { + "album": "From The Depths", + "album_id": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths#p1459084917", + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "tracks": [ + { + "title": "Conflict", + "track_id": "https://hexbarcelona.bandcamp.com/track/conflict", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 328, + "index": 1, + "media": "Vinyl", + "medium": 1, + "medium_index": 1, + "medium_total": 5, + "artist_sort": null, + "disctitle": "Limited 12\" Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "A1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Arise", + "track_id": "https://hexbarcelona.bandcamp.com/track/arise", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 298, + "index": 2, + "media": "Vinyl", + "medium": 1, + "medium_index": 2, + "medium_total": 5, + "artist_sort": null, + "disctitle": "Limited 12\" Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "A2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Timeless", + "track_id": "https://hexbarcelona.bandcamp.com/track/timeless", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 445, + "index": 3, + "media": "Vinyl", + "medium": 1, + "medium_index": 3, + "medium_total": 5, + "artist_sort": null, + "disctitle": "Limited 12\" Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "A3", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "END", + "track_id": "https://hexbarcelona.bandcamp.com/track/end", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 312, + "index": 4, + "media": "Vinyl", + "medium": 1, + "medium_index": 4, + "medium_total": 5, + "artist_sort": null, + "disctitle": "Limited 12\" Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "B1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Conflict (NX1 Remix)", + "track_id": "https://hexbarcelona.bandcamp.com/track/conflict-nx1-remix", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 331, + "index": 5, + "media": "Vinyl", + "medium": 1, + "medium_index": 5, + "medium_total": 5, + "artist_sort": null, + "disctitle": "Limited 12\" Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "B2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + } + ], + "asin": null, + "albumtype": "ep", + "va": false, + "year": 2019, + "month": 10, + "day": 4, + "label": "HEX Recordings - Techno movement", + "mediums": 1, + "artist_sort": null, + "releasegroup_id": null, + "catalognum": "HEX002", + "script": null, + "language": null, + "country": "ES", + "style": "electronic", + "genre": "techno", + "albumstatus": "Official", + "media": "Vinyl", + "albumdisambig": null, + "releasegroupdisambig": null, + "artist_credit": null, + "original_year": null, + "original_month": null, + "original_day": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "discogs_albumid": null, + "discogs_labelid": null, + "discogs_artistid": null, + "albumtypes": "ep", + "comments": "‘Conflict’ opens the release with fast paced percussion, sustained atmospherics and frenzied high hats before ‘Arise’ weaves acid dipped synths into metallic clashes and hard hitting stabs. ’Timeless’ is next, painting haunting textures with stormy effects, leading into ‘END’ which swiftly builds into a powerful cut complete with growling bass. Tying everything together, Nexe Records founders NX1 remix ‘Conflict’, incorporating warped frequencies and a murky low-end.\n\nFeedbacks about the EP:\n► Rebekah - killer industrial vibes here, love all tracks and remix\n► Gary Beck - These are huge, especially the timeless one\n► Dasha Rush - like most of the tracks, thanks\n► Under Black Helmet - The whole EP is great!\n► Randomer - great tracks\n► Remco Beekwilder - Conflict is a belter, thanks!!\n► Raffaele Attanasio - I'll play for sure!!!\n► Henning Baer - TOP!\n► Stephanie Sykes - Yaaaaaasssss.... loving this release!! <3\n► Insolate - Arise super cool!\n► Joe Farr - NX1 remix is great. I also like Timeless a lot. Thanks.\n► Nur Jaber - VII <3\n► Cleric - Nice EP will play thanks\n\nand many more...\n\nMastered at The Bass Valley Studios by Alan Lockwood\n---\nHEX002 - From The Depths\n1x12\"\n180 grams\n\nA1 - Conflict\nA2 - Arise\nA3 - Conflict (NX1 Remix)\nB1 - Timeless\nB2 - END" + } +] diff --git a/tests/json/expected/hex008.json b/tests/json/expected/hex008.json new file mode 100644 index 0000000..506f4a0 --- /dev/null +++ b/tests/json/expected/hex008.json @@ -0,0 +1,834 @@ +[ + { + "album": "Angels From Hell", + "album_id": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "artist": "Various Artists", + "artist_id": "https://hexbarcelona.bandcamp.com", + "tracks": [ + { + "title": "You Be The Leader", + "track_id": "https://hexbarcelona.bandcamp.com/track/a1-rebekah-you-be-the-leader", + "release_track_id": null, + "artist": "Rebekah", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 302, + "index": 1, + "media": "Digital Media", + "medium": 1, + "medium_index": 1, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "A1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Sanitary Dictature", + "track_id": "https://hexbarcelona.bandcamp.com/track/a2-paolo-ferrara-lorenzo-raganzini-sanitary-dictature", + "release_track_id": null, + "artist": "Paolo Ferrara & Lorenzo Raganzini", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 397, + "index": 2, + "media": "Digital Media", + "medium": 1, + "medium_index": 2, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "A2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "12 Gauge", + "track_id": "https://hexbarcelona.bandcamp.com/track/a3-aeit-12-gauge", + "release_track_id": null, + "artist": "AEIT", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 307, + "index": 3, + "media": "Digital Media", + "medium": 1, + "medium_index": 3, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "A3", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Demons", + "track_id": "https://hexbarcelona.bandcamp.com/track/b1-vii-circle-demons", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 413, + "index": 4, + "media": "Digital Media", + "medium": 1, + "medium_index": 4, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "B1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Mezcal Worm", + "track_id": "https://hexbarcelona.bandcamp.com/track/b2-rommek-mezcal-worm", + "release_track_id": null, + "artist": "Rommek", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 397, + "index": 5, + "media": "Digital Media", + "medium": 1, + "medium_index": 5, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "B2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "The Puppet Master", + "track_id": "https://hexbarcelona.bandcamp.com/track/c1-cleric-the-puppet-master", + "release_track_id": null, + "artist": "Cleric", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 367, + "index": 6, + "media": "Digital Media", + "medium": 1, + "medium_index": 6, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "C1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Air Of The 90s", + "track_id": "https://hexbarcelona.bandcamp.com/track/c2-remco-beekwilder-air-of-the-90s", + "release_track_id": null, + "artist": "Remco Beekwilder", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 399, + "index": 7, + "media": "Digital Media", + "medium": 1, + "medium_index": 7, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "C2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Fearless", + "track_id": "https://hexbarcelona.bandcamp.com/track/d1-and-fearless", + "release_track_id": null, + "artist": "AnD", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 436, + "index": 8, + "media": "Digital Media", + "medium": 1, + "medium_index": 8, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "D1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Matriachy", + "track_id": "https://hexbarcelona.bandcamp.com/track/d2-benjamin-damage-matriachy", + "release_track_id": null, + "artist": "Benjamin Damage", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 387, + "index": 9, + "media": "Digital Media", + "medium": 1, + "medium_index": 9, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "D2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": " The Purist", + "track_id": "https://hexbarcelona.bandcamp.com/track/e1-under-black-helmet-the-purist", + "release_track_id": null, + "artist": "Under Black Helmet ", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 305, + "index": 10, + "media": "Digital Media", + "medium": 1, + "medium_index": 10, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "E1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Organisms", + "track_id": "https://hexbarcelona.bandcamp.com/track/e2-maere-6siss-organisms", + "release_track_id": null, + "artist": "MAERE & 6SISS", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 376, + "index": 11, + "media": "Digital Media", + "medium": 1, + "medium_index": 11, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "E2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Broken Tools", + "track_id": "https://hexbarcelona.bandcamp.com/track/f1-imperial-black-unit-broken-tools", + "release_track_id": null, + "artist": "Imperial Black Unit", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 363, + "index": 12, + "media": "Digital Media", + "medium": 1, + "medium_index": 12, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "F1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Ctrl+Alt+Delete The System", + "track_id": "https://hexbarcelona.bandcamp.com/track/f2-end-train-ctrl-alt-delete-the-system", + "release_track_id": null, + "artist": "End Train", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 387, + "index": 13, + "media": "Digital Media", + "medium": 1, + "medium_index": 13, + "medium_total": 13, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "F2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + } + ], + "asin": null, + "albumtype": "compilation", + "va": true, + "year": 2021, + "month": 9, + "day": 1, + "label": "HEX Recordings - Techno movement", + "mediums": 1, + "artist_sort": null, + "releasegroup_id": null, + "catalognum": "HEX008", + "script": null, + "language": null, + "country": "ES", + "style": "electronic", + "genre": "techno", + "albumstatus": "Official", + "media": "Digital Media", + "albumdisambig": null, + "releasegroupdisambig": null, + "artist_credit": null, + "original_year": null, + "original_month": null, + "original_day": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "discogs_albumid": null, + "discogs_labelid": null, + "discogs_artistid": null, + "albumtypes": "album; compilation", + "comments": "HEX008 | Angels From Hell (Various Artists)\nTriple-Red-Vinyl\n\n---\n\nSome feedback on the release:\n\n► Quail - HUGE pack, absolute killers all round. Thanks!\n► Essan - Thank you very much for sending me your promo, the tracks sound amazing, I love!!!!\n► JC Laurent - Super Solid VA! Thanks a lot :) Rommek & Cleric are my favs\n► boyd schidt - sick release thanks for the promo\n► Downwell - Great tracks ! Fav: Imperial Black Unit - Broken Tools\n► Jay Clarke - Heavy Heavy selection of tunes here. Solid release chaps! Thx for sharing!\n► takaaki itoh - great v.a!\n► slam - nice collection\n► Henning Baer - Massive!\n► Cristian Varela - AWESOME!\n► Roll Dann - Crazy Tracks, muchas gracias!\n► Luca Agnelli - super pack\n► ENDLEC - Superb selections as usual!\n► Jerm - Cleric's & Benjamin's my top picks, great compilation overall guys :)\n► Tom Page - Loads here for me, fantastic compilation! Love the Rommek, Benjamin Damage, Cleric & Imperial Black Unit tracks, but its all top notch, thanks :)\n► Alex Guerra - Brutal,Friends! Congrats for 2nd Anniversary.Thanks again!\n\n---\n\nWe're very excited to celebrate the 2nd Anniversary of \"HEX Recordings\" so for the occasion, we decided to create the most powerful release so far, a Triple-Vinyl Various Artists, which includes the creations of some of the artists that are closest to the Movement but never released on the label until now like Rebekah, Cleric, Remco Beekwilder, together with new members like Benjamin Damage, AEIT, Imperial Black Unit, End Train, Rommek, Maere, 6Siss and of course some of the ambassadors of the label like Under Black Helmet, VII Circle and the two HEX Founders Paolo Ferrara and Lorenzo Raganzini.\n\nThe title of the release is \"Angels From Hell\" and it represents the most ambitious release ever created by HEX. 3 flaming Red colour Vinyl, together with some new T-shirts + Hoodie characterised by the VA's unique design.\n\n15 Artists will represent the unity under Techno of 7 different countries: France, England, Italy, Netherlands, Lithuania, Belgium and Israel.\n\n---\n\nVinyl 1\nA1 - Rebekah - You Be The Leader\nA2 - Paolo Ferrara & Lorenzo Raganzini - Sanitary Dictature\nA3 - AEIT - 12 Gauge\nB1 - VII Circle - Demons\nB2 - Rommek - Mezcal Worm\n\n\nVinyl 2\nC1 - Cleric - The Puppet Master \nC2 - Remco Beekwilder - Air Of The 90s\nD1 - AnD - Fearless\nD2 - Benjamin Damage - Matriachy\n\n\nVinyl 3 \nE1 - Under Black Helmet - The Purist\nE2 - MAERE & 6SISS - Organisms\nF1 - Imperial Black Unit - Broken Tools\nF2 - End Train - Ctrl+Alt+Delete The System\n\n\nMasters by Alain Paul (Germany)\nDesign by Giambrone Studio" + }, + { + "album": "Angels From Hell", + "album_id": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists#p3220875979", + "artist": "Various Artists", + "artist_id": "https://hexbarcelona.bandcamp.com", + "tracks": [ + { + "title": "You Be The Leader", + "track_id": "https://hexbarcelona.bandcamp.com/track/a1-rebekah-you-be-the-leader", + "release_track_id": null, + "artist": "Rebekah", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 302, + "index": 1, + "media": "Vinyl", + "medium": 1, + "medium_index": 1, + "medium_total": 5, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "A1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Sanitary Dictature", + "track_id": "https://hexbarcelona.bandcamp.com/track/a2-paolo-ferrara-lorenzo-raganzini-sanitary-dictature", + "release_track_id": null, + "artist": "Paolo Ferrara & Lorenzo Raganzini", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 397, + "index": 2, + "media": "Vinyl", + "medium": 1, + "medium_index": 2, + "medium_total": 5, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "A2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "12 Gauge", + "track_id": "https://hexbarcelona.bandcamp.com/track/a3-aeit-12-gauge", + "release_track_id": null, + "artist": "AEIT", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 307, + "index": 3, + "media": "Vinyl", + "medium": 1, + "medium_index": 3, + "medium_total": 5, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "A3", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Demons", + "track_id": "https://hexbarcelona.bandcamp.com/track/b1-vii-circle-demons", + "release_track_id": null, + "artist": "VII Circle", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 413, + "index": 4, + "media": "Vinyl", + "medium": 1, + "medium_index": 4, + "medium_total": 5, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "B1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Mezcal Worm", + "track_id": "https://hexbarcelona.bandcamp.com/track/b2-rommek-mezcal-worm", + "release_track_id": null, + "artist": "Rommek", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 397, + "index": 5, + "media": "Vinyl", + "medium": 1, + "medium_index": 5, + "medium_total": 5, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "B2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "The Puppet Master", + "track_id": "https://hexbarcelona.bandcamp.com/track/c1-cleric-the-puppet-master", + "release_track_id": null, + "artist": "Cleric", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 367, + "index": 6, + "media": "Vinyl", + "medium": 2, + "medium_index": 1, + "medium_total": 4, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "C1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Air Of The 90s", + "track_id": "https://hexbarcelona.bandcamp.com/track/c2-remco-beekwilder-air-of-the-90s", + "release_track_id": null, + "artist": "Remco Beekwilder", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 399, + "index": 7, + "media": "Vinyl", + "medium": 2, + "medium_index": 2, + "medium_total": 4, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "C2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Fearless", + "track_id": "https://hexbarcelona.bandcamp.com/track/d1-and-fearless", + "release_track_id": null, + "artist": "AnD", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 436, + "index": 8, + "media": "Vinyl", + "medium": 2, + "medium_index": 3, + "medium_total": 4, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "D1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Matriachy", + "track_id": "https://hexbarcelona.bandcamp.com/track/d2-benjamin-damage-matriachy", + "release_track_id": null, + "artist": "Benjamin Damage", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 387, + "index": 9, + "media": "Vinyl", + "medium": 2, + "medium_index": 4, + "medium_total": 4, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "D2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": " The Purist", + "track_id": "https://hexbarcelona.bandcamp.com/track/e1-under-black-helmet-the-purist", + "release_track_id": null, + "artist": "Under Black Helmet ", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 305, + "index": 10, + "media": "Vinyl", + "medium": 3, + "medium_index": 1, + "medium_total": 4, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "E1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Organisms", + "track_id": "https://hexbarcelona.bandcamp.com/track/e2-maere-6siss-organisms", + "release_track_id": null, + "artist": "MAERE & 6SISS", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 376, + "index": 11, + "media": "Vinyl", + "medium": 3, + "medium_index": 2, + "medium_total": 4, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "E2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Broken Tools", + "track_id": "https://hexbarcelona.bandcamp.com/track/f1-imperial-black-unit-broken-tools", + "release_track_id": null, + "artist": "Imperial Black Unit", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 363, + "index": 12, + "media": "Vinyl", + "medium": 3, + "medium_index": 3, + "medium_total": 4, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "F1", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Ctrl+Alt+Delete The System", + "track_id": "https://hexbarcelona.bandcamp.com/track/f2-end-train-ctrl-alt-delete-the-system", + "release_track_id": null, + "artist": "End Train", + "artist_id": "https://hexbarcelona.bandcamp.com", + "length": 387, + "index": 13, + "media": "Vinyl", + "medium": 3, + "medium_index": 4, + "medium_total": 4, + "artist_sort": null, + "disctitle": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": "F2", + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + } + ], + "asin": null, + "albumtype": "compilation", + "va": true, + "year": 2021, + "month": 9, + "day": 1, + "label": "HEX Recordings - Techno movement", + "mediums": 3, + "artist_sort": null, + "releasegroup_id": null, + "catalognum": "XXX008", + "script": null, + "language": null, + "country": "ES", + "style": "electronic", + "genre": "techno", + "albumstatus": "Official", + "media": "Vinyl", + "albumdisambig": null, + "releasegroupdisambig": null, + "artist_credit": null, + "original_year": null, + "original_month": null, + "original_day": null, + "data_source": "bandcamp", + "data_url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "discogs_albumid": null, + "discogs_labelid": null, + "discogs_artistid": null, + "albumtypes": "album; compilation", + "comments": "HEX008 | Angels From Hell (Various Artists)\nTriple-Red-Vinyl\n\n---\n\nSome feedback on the release:\n\n► Quail - HUGE pack, absolute killers all round. Thanks!\n► Essan - Thank you very much for sending me your promo, the tracks sound amazing, I love!!!!\n► JC Laurent - Super Solid VA! Thanks a lot :) Rommek & Cleric are my favs\n► boyd schidt - sick release thanks for the promo\n► Downwell - Great tracks ! Fav: Imperial Black Unit - Broken Tools\n► Jay Clarke - Heavy Heavy selection of tunes here. Solid release chaps! Thx for sharing!\n► takaaki itoh - great v.a!\n► slam - nice collection\n► Henning Baer - Massive!\n► Cristian Varela - AWESOME!\n► Roll Dann - Crazy Tracks, muchas gracias!\n► Luca Agnelli - super pack\n► ENDLEC - Superb selections as usual!\n► Jerm - Cleric's & Benjamin's my top picks, great compilation overall guys :)\n► Tom Page - Loads here for me, fantastic compilation! Love the Rommek, Benjamin Damage, Cleric & Imperial Black Unit tracks, but its all top notch, thanks :)\n► Alex Guerra - Brutal,Friends! Congrats for 2nd Anniversary.Thanks again!\n\n---\n\nWe're very excited to celebrate the 2nd Anniversary of \"HEX Recordings\" so for the occasion, we decided to create the most powerful release so far, a Triple-Vinyl Various Artists, which includes the creations of some of the artists that are closest to the Movement but never released on the label until now like Rebekah, Cleric, Remco Beekwilder, together with new members like Benjamin Damage, AEIT, Imperial Black Unit, End Train, Rommek, Maere, 6Siss and of course some of the ambassadors of the label like Under Black Helmet, VII Circle and the two HEX Founders Paolo Ferrara and Lorenzo Raganzini.\n\nThe title of the release is \"Angels From Hell\" and it represents the most ambitious release ever created by HEX. 3 flaming Red colour Vinyl, together with some new T-shirts + Hoodie characterised by the VA's unique design.\n\n15 Artists will represent the unity under Techno of 7 different countries: France, England, Italy, Netherlands, Lithuania, Belgium and Israel.\n\n---\n\nVinyl 1\nA1 - Rebekah - You Be The Leader\nA2 - Paolo Ferrara & Lorenzo Raganzini - Sanitary Dictature\nA3 - AEIT - 12 Gauge\nB1 - VII Circle - Demons\nB2 - Rommek - Mezcal Worm\n\n\nVinyl 2\nC1 - Cleric - The Puppet Master \nC2 - Remco Beekwilder - Air Of The 90s\nD1 - AnD - Fearless\nD2 - Benjamin Damage - Matriachy\n\n\nVinyl 3 \nE1 - Under Black Helmet - The Purist\nE2 - MAERE & 6SISS - Organisms\nF1 - Imperial Black Unit - Broken Tools\nF2 - End Train - Ctrl+Alt+Delete The System\n\n\nMasters by Alain Paul (Germany)\nDesign by Giambrone Studio\n---\nTriple-Red Flamed-Vinyl\n(only 300 copies)\n\nNo black version\n\nXXX008 | Angels From Hell\n3x12\" special red edition" + } +] diff --git a/tests/json/expected/issue-18.json b/tests/json/expected/issue-18.json index 62be0d3..ca7b0da 100644 --- a/tests/json/expected/issue-18.json +++ b/tests/json/expected/issue-18.json @@ -12,7 +12,7 @@ "artist_sort": null, "asin": null, "catalognum": "", - "comments": "", + "comments": null, "country": "US", "data_source": "bandcamp", "data_url": "https://54thregiment.bandcamp.com/album/fall-in", diff --git a/tests/json/expected/remix_without_brackets.json b/tests/json/expected/remix_without_brackets.json index a42b720..8130ab6 100644 --- a/tests/json/expected/remix_without_brackets.json +++ b/tests/json/expected/remix_without_brackets.json @@ -1 +1,128 @@ -[{"album": "Problem Child EP", "album_id": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", "artist": "Aluphobia, Astatine", "artist_id": "https://purehate000.bandcamp.com", "tracks": [{"title": "Problem Child", "track_id": "https://purehate000.bandcamp.com/track/aluphobia-x-astatine-problem-child", "release_track_id": null, "artist": "Aluphobia x Astatine", "artist_id": "https://purehate000.bandcamp.com", "length": 312, "index": 1, "media": "Digital Media", "medium": 1, "medium_index": 1, "medium_total": 3, "artist_sort": null, "disctitle": null, "artist_credit": null, "data_source": "bandcamp", "data_url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", "lyricist": null, "composer": null, "composer_sort": null, "arranger": null, "track_alt": null, "work": null, "mb_workid": null, "work_disambig": null, "bpm": null, "initial_key": null, "genre": null}, {"title": "Problem Child (Illiya Korniyen Remix)", "track_id": "https://purehate000.bandcamp.com/track/aluphobia-x-astatine-problem-child-illiya-korniyen-remix", "release_track_id": null, "artist": "Aluphobia x Astatine", "artist_id": "https://purehate000.bandcamp.com", "length": 395, "index": 2, "media": "Digital Media", "medium": 1, "medium_index": 2, "medium_total": 3, "artist_sort": null, "disctitle": null, "artist_credit": null, "data_source": "bandcamp", "data_url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", "lyricist": null, "composer": null, "composer_sort": null, "arranger": null, "track_alt": null, "work": null, "mb_workid": null, "work_disambig": null, "bpm": null, "initial_key": null, "genre": null}, {"title": "Problem Child (Korcs Remix)", "track_id": "https://purehate000.bandcamp.com/track/aluphobia-x-astatine-problem-child-korcs-remix", "release_track_id": null, "artist": "Aluphobia x Astatine", "artist_id": "https://purehate000.bandcamp.com", "length": 391, "index": 3, "media": "Digital Media", "medium": 1, "medium_index": 3, "medium_total": 3, "artist_sort": null, "disctitle": null, "artist_credit": null, "data_source": "bandcamp", "data_url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", "lyricist": null, "composer": null, "composer_sort": null, "arranger": null, "track_alt": null, "work": null, "mb_workid": null, "work_disambig": null, "bpm": null, "initial_key": null, "genre": null}], "asin": null, "albumtype": "ep", "va": false, "year": 2021, "month": 12, "day": 10, "label": "PURE HATE", "mediums": 1, "artist_sort": null, "releasegroup_id": null, "catalognum": "", "script": null, "language": null, "country": "HU", "style": "electronic", "genre": "techno", "albumstatus": "Official", "media": "Digital Media", "albumdisambig": null, "releasegroupdisambig": null, "artist_credit": null, "original_year": null, "original_month": null, "original_day": null, "data_source": "bandcamp", "data_url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", "discogs_albumid": null, "discogs_labelid": null, "discogs_artistid": null, "comments": "", "albumtypes": "ep; single"}] +[ + { + "album": "Problem Child EP", + "album_id": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", + "artist": "Aluphobia, Astatine", + "artist_id": "https://purehate000.bandcamp.com", + "tracks": [ + { + "title": "Problem Child", + "track_id": "https://purehate000.bandcamp.com/track/aluphobia-x-astatine-problem-child", + "release_track_id": null, + "artist": "Aluphobia x Astatine", + "artist_id": "https://purehate000.bandcamp.com", + "length": 312, + "index": 1, + "media": "Digital Media", + "medium": 1, + "medium_index": 1, + "medium_total": 3, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": null, + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Problem Child (Illiya Korniyen Remix)", + "track_id": "https://purehate000.bandcamp.com/track/aluphobia-x-astatine-problem-child-illiya-korniyen-remix", + "release_track_id": null, + "artist": "Aluphobia x Astatine", + "artist_id": "https://purehate000.bandcamp.com", + "length": 395, + "index": 2, + "media": "Digital Media", + "medium": 1, + "medium_index": 2, + "medium_total": 3, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": null, + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + }, + { + "title": "Problem Child (Korcs Remix)", + "track_id": "https://purehate000.bandcamp.com/track/aluphobia-x-astatine-problem-child-korcs-remix", + "release_track_id": null, + "artist": "Aluphobia x Astatine", + "artist_id": "https://purehate000.bandcamp.com", + "length": 391, + "index": 3, + "media": "Digital Media", + "medium": 1, + "medium_index": 3, + "medium_total": 3, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": null, + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + } + ], + "asin": null, + "albumtype": "ep", + "va": false, + "year": 2021, + "month": 12, + "day": 10, + "label": "PURE HATE", + "mediums": 1, + "artist_sort": null, + "releasegroup_id": null, + "catalognum": "", + "script": null, + "language": null, + "country": "HU", + "style": "electronic", + "genre": "techno", + "albumstatus": "Official", + "media": "Digital Media", + "albumdisambig": null, + "releasegroupdisambig": null, + "artist_credit": null, + "original_year": null, + "original_month": null, + "original_day": null, + "data_source": "bandcamp", + "data_url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep", + "discogs_albumid": null, + "discogs_labelid": null, + "discogs_artistid": null, + "comments": null, + "albumtypes": "ep; single" + } +] diff --git a/tests/json/expected/rr2.json b/tests/json/expected/rr2.json new file mode 100644 index 0000000..77e2534 --- /dev/null +++ b/tests/json/expected/rr2.json @@ -0,0 +1,70 @@ +[ + { + "album": "RR2", + "album_id": "https://44labelgroup.bandcamp.com/album/rr2", + "artist": "RADICAL G & THE HORRORIST", + "artist_id": "https://44labelgroup.bandcamp.com", + "tracks": [ + { + "title": "Here Comes The Storm (Kobosil 44 Terror Mix)", + "track_id": "https://44labelgroup.bandcamp.com/track/here-comes-the-storm-kobosil-44-terror-mix", + "release_track_id": null, + "artist": "RADICAL G & THE HORRORIST", + "artist_id": "https://44labelgroup.bandcamp.com", + "length": 370, + "index": 1, + "media": "Digital Media", + "medium": 1, + "medium_index": 1, + "medium_total": 1, + "artist_sort": null, + "disctitle": null, + "artist_credit": null, + "data_source": "bandcamp", + "data_url": "https://44labelgroup.bandcamp.com/album/rr2", + "lyricist": null, + "composer": null, + "composer_sort": null, + "arranger": null, + "track_alt": null, + "work": null, + "mb_workid": null, + "work_disambig": null, + "bpm": null, + "initial_key": null, + "genre": null + } + ], + "asin": null, + "albumtype": "album", + "va": false, + "year": 2019, + "month": 1, + "day": 28, + "label": "44 LABEL GROUP", + "mediums": 1, + "artist_sort": null, + "releasegroup_id": null, + "catalognum": "", + "script": null, + "language": null, + "country": "DE", + "style": null, + "genre": null, + "albumstatus": "Official", + "media": "Digital Media", + "albumdisambig": null, + "releasegroupdisambig": null, + "artist_credit": null, + "original_year": null, + "original_month": null, + "original_day": null, + "data_source": "bandcamp", + "data_url": "https://44labelgroup.bandcamp.com/album/rr2", + "discogs_albumid": null, + "discogs_labelid": null, + "discogs_artistid": null, + "albumtypes": "album; remix; single", + "comments": null + } +] diff --git a/tests/json/expected/single_only_track_name.json b/tests/json/expected/single_only_track_name.json index 76313f3..f62c25e 100644 --- a/tests/json/expected/single_only_track_name.json +++ b/tests/json/expected/single_only_track_name.json @@ -8,7 +8,7 @@ "artist_sort": null, "bpm": null, "catalognum": "", - "comments": "", + "comments": "little distorted", "composer": null, "composer_sort": null, "country": "XW", @@ -30,7 +30,7 @@ "month": 1, "release_track_id": null, "style": "electronic", - "title": "oenera", + "title": "OENERA", "track_alt": null, "track_id": "https://gutkeinforu.bandcamp.com/track/oenera", "work": null, diff --git a/tests/json/hex002.json b/tests/json/hex002.json new file mode 100644 index 0000000..c5d398f --- /dev/null +++ b/tests/json/hex002.json @@ -0,0 +1,472 @@ +{ + "@context": "https://schema.org", + "@id": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "@type": "MusicAlbum", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "art_id", + "value": 3115894657 + }, + { + "@type": "PropertyValue", + "name": "featured_track_num", + "value": 1 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "albumRelease": [ + { + "@id": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "@type": [ + "MusicRelease", + "Product" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "item_id", + "value": 2521852486 + }, + { + "@type": "PropertyValue", + "name": "item_type", + "value": "a" + }, + { + "@type": "PropertyValue", + "name": "selling_band_id", + "value": 1252439829 + }, + { + "@type": "PropertyValue", + "name": "type_name", + "value": "Digital" + }, + { + "@type": "PropertyValue", + "name": "art_id", + "value": 3115894657 + } + ], + "description": "Includes high-quality download in MP3, FLAC and more. Paying supporters also get unlimited streaming via the free Bandcamp app.", + "image": [ + "https://f4.bcbits.com/img/a3115894657_10.jpg" + ], + "musicReleaseFormat": "DigitalFormat", + "name": "HEX002 | From The Depths" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths#b59258785", + "@type": [ + "MusicRelease", + "Product" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "item_id", + "value": 59258785 + }, + { + "@type": "PropertyValue", + "name": "item_type", + "value": "b" + }, + { + "@type": "PropertyValue", + "name": "selling_band_id", + "value": 1252439829 + }, + { + "@type": "PropertyValue", + "name": "type_name", + "value": "Digital" + }, + { + "@type": "PropertyValue", + "name": "art_id", + "value": 464287237 + }, + { + "@type": "PropertyValue", + "name": "is_bfd", + "value": true + } + ], + "image": [ + "https://f4.bcbits.com/img/a0464287237_10.jpg" + ], + "musicReleaseFormat": "DigitalFormat", + "name": "full digital discography (48 releases)", + "offers": { + "@type": "Offer", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "bundle_size", + "value": 48 + }, + { + "@type": "PropertyValue", + "name": "discount", + "value": 0.5 + } + ], + "availability": "OnlineOnly", + "price": 154.7, + "priceCurrency": "EUR", + "priceSpecification": { + "minPrice": 154.7 + }, + "url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths#b59258785-buy" + } + }, + { + "@id": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths#p1459084917", + "@type": [ + "MusicRelease", + "Product" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "item_id", + "value": 1459084917 + }, + { + "@type": "PropertyValue", + "name": "item_type", + "value": "p" + }, + { + "@type": "PropertyValue", + "name": "selling_band_id", + "value": 1252439829 + }, + { + "@type": "PropertyValue", + "name": "type_name", + "value": "Vinyl LP" + }, + { + "@type": "PropertyValue", + "name": "image_ids", + "value": [ + 19283270, + 19283333 + ] + }, + { + "@type": "PropertyValue", + "name": "is_music_merch", + "value": true + }, + { + "@type": "PropertyValue", + "name": "type_id", + "value": 2 + } + ], + "description": "HEX002 - From The Depths\n1x12\"\n180 grams\n\nA1 - Conflict\nA2 - Arise\nA3 - Conflict (NX1 Remix)\nB1 - Timeless\nB2 - END", + "image": [ + "https://f4.bcbits.com/img/0019283270_10.jpg", + "https://f4.bcbits.com/img/0019283333_10.jpg" + ], + "musicReleaseFormat": "VinylFormat", + "name": "Limited 12\" Vinyl", + "offers": { + "@type": "Offer", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "includes_digital_download", + "value": true + } + ], + "availability": "SoldOut", + "price": 11.95, + "priceCurrency": "EUR", + "priceSpecification": { + "minPrice": 11.95 + }, + "url": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths#p1459084917-buy" + } + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/conflict", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/arise", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/timeless", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/end", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/conflict-nx1-remix", + "@type": "MusicRelease" + } + ], + "albumReleaseType": "AlbumRelease", + "byArtist": { + "@type": "MusicGroup", + "name": "VII Circle, NX1" + }, + "copyrightNotice": "All Rights Reserved", + "dateModified": "19 Nov 2022 22:42:50 GMT", + "datePublished": "04 Oct 2019 00:00:00 GMT", + "description": "‘Conflict’ opens the release with fast paced percussion, sustained atmospherics and frenzied high hats before ‘Arise’ weaves acid dipped synths into metallic clashes and hard hitting stabs. ’Timeless’ is next, painting haunting textures with stormy effects, leading into ‘END’ which swiftly builds into a powerful cut complete with growling bass. Tying everything together, Nexe Records founders NX1 remix ‘Conflict’, incorporating warped frequencies and a murky low-end.\r\n\r\nFeedbacks about the EP:\r\n► Rebekah - killer industrial vibes here, love all tracks and remix\r\n► Gary Beck - These are huge, especially the timeless one\r\n► Dasha Rush - like most of the tracks, thanks\r\n► Under Black Helmet - The whole EP is great!\r\n► Randomer - great tracks\r\n► Remco Beekwilder - Conflict is a belter, thanks!!\r\n► Raffaele Attanasio - I'll play for sure!!!\r\n► Henning Baer - TOP!\r\n► Stephanie Sykes - Yaaaaaasssss.... loving this release!! <3\r\n► Insolate - Arise super cool!\r\n► Joe Farr - NX1 remix is great. I also like Timeless a lot. Thanks.\r\n► Nur Jaber - VII <3\r\n► Cleric - Nice EP will play thanks\r\n\r\nand many more...\r\n\r\nMastered at The Bass Valley Studios by Alan Lockwood", + "image": "https://f4.bcbits.com/img/a3115894657_10.jpg", + "keywords": [ + "Electronic", + "Techno", + "techno", + "Barcelona" + ], + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/album/hex002-from-the-depths", + "name": "HEX002 | From The Depths", + "numTracks": 5, + "publisher": { + "@id": "https://hexbarcelona.bandcamp.com", + "@type": "MusicGroup", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "band_id", + "value": 1252439829 + }, + { + "@type": "PropertyValue", + "name": "has_any_downloads", + "value": true + }, + { + "@type": "PropertyValue", + "name": "has_download_codes", + "value": true + }, + { + "@type": "PropertyValue", + "name": "has_policies", + "value": true + }, + { + "@type": "PropertyValue", + "name": "image_height", + "value": 1200 + }, + { + "@type": "PropertyValue", + "name": "image_id", + "value": 23279504 + }, + { + "@type": "PropertyValue", + "name": "image_width", + "value": 1200 + } + ], + "description": "TechnoMetal label founded in 2019 by Lorenzo Raganzini and Paolo Ferrara.\n\nIt is related to HEX Techno movement.\n\nDEMOS\ndemos@hex-technomovement.com", + "foundingLocation": { + "@type": "Place", + "name": "Barcelona, Spain" + }, + "genre": "https://bandcamp.com/discover/electronic", + "image": "https://f4.bcbits.com/img/0023279504_10.jpg", + "mainEntityOfPage": [ + { + "@type": "WebSite", + "name": "hex-technomovement.com", + "url": "http://www.hex-technomovement.com" + } + ], + "name": "HEX Recordings - Techno movement", + "subjectOf": [ + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "m" + } + ], + "name": "Digital Music", + "url": "https://hexbarcelona.bandcamp.com/music" + }, + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "p" + } + ], + "name": "Vinyl Store & Merch", + "url": "https://hexbarcelona.bandcamp.com/merch" + }, + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "v" + } + ], + "name": "Videoclips", + "url": "https://hexbarcelona.bandcamp.com/video" + }, + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "c" + } + ], + "name": "Community", + "url": "https://hexbarcelona.bandcamp.com/community" + } + ] + }, + "track": { + "@type": "ItemList", + "itemListElement": [ + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/conflict", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 3484674383 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H05M28S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/conflict", + "name": "Conflict" + }, + "position": 1 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/arise", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 3249169829 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H04M58S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/arise", + "name": "Arise" + }, + "position": 2 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/timeless", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 2481022120 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H07M25S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/timeless", + "name": "Timeless" + }, + "position": 3 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/end", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 3006487739 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H05M12S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/end", + "name": "END" + }, + "position": 4 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/conflict-nx1-remix", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 2051893236 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H05M31S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/conflict-nx1-remix", + "name": "Conflict (NX1 Remix)" + }, + "position": 5 + } + ], + "numberOfItems": 5 + } +} diff --git a/tests/json/hex008.json b/tests/json/hex008.json new file mode 100644 index 0000000..7bb3af9 --- /dev/null +++ b/tests/json/hex008.json @@ -0,0 +1,756 @@ +{ + "@context": "https://schema.org", + "@id": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "@type": "MusicAlbum", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "art_id", + "value": 1834646812 + }, + { + "@type": "PropertyValue", + "name": "featured_track_num", + "value": 1 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "albumRelease": [ + { + "@id": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "@type": [ + "MusicRelease", + "Product" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "item_id", + "value": 4192984768 + }, + { + "@type": "PropertyValue", + "name": "item_type", + "value": "a" + }, + { + "@type": "PropertyValue", + "name": "selling_band_id", + "value": 1252439829 + }, + { + "@type": "PropertyValue", + "name": "type_name", + "value": "Digital" + }, + { + "@type": "PropertyValue", + "name": "art_id", + "value": 1834646812 + } + ], + "description": "Includes high-quality download in MP3, FLAC and more. Paying supporters also get unlimited streaming via the free Bandcamp app.", + "image": [ + "https://f4.bcbits.com/img/a1834646812_10.jpg" + ], + "musicReleaseFormat": "DigitalFormat", + "name": "HEX008 | Angels From Hell (Various Artists)" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists#p3220875979", + "@type": [ + "MusicRelease", + "Product" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "item_id", + "value": 3220875979 + }, + { + "@type": "PropertyValue", + "name": "item_type", + "value": "p" + }, + { + "@type": "PropertyValue", + "name": "selling_band_id", + "value": 1252439829 + }, + { + "@type": "PropertyValue", + "name": "type_name", + "value": "Vinyl LP" + }, + { + "@type": "PropertyValue", + "name": "image_ids", + "value": [ + 25073911, + 25073924, + 25073937 + ] + }, + { + "@type": "PropertyValue", + "name": "is_music_merch", + "value": true + }, + { + "@type": "PropertyValue", + "name": "type_id", + "value": 2 + } + ], + "description": "Triple-Red Flamed-Vinyl\n(only 300 copies)\n\nNo black version\n\nXXX008 | Angels From Hell\n3x12\" special red edition", + "image": [ + "https://f4.bcbits.com/img/0025073911_10.jpg", + "https://f4.bcbits.com/img/0025073924_10.jpg", + "https://f4.bcbits.com/img/0025073937_10.jpg" + ], + "musicReleaseFormat": "VinylFormat", + "name": "XXX008 | Angels From Hell (Various Artists) Triple-Vinyl", + "offers": { + "@type": "Offer", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "includes_digital_download", + "value": true + } + ], + "availability": "InStock", + "price": 37.95, + "priceCurrency": "EUR", + "priceSpecification": { + "minPrice": 37.95 + }, + "url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists#p3220875979-buy" + } + }, + { + "@id": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists#p3583995757", + "@type": [ + "MusicRelease", + "Product" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "item_id", + "value": 3583995757 + }, + { + "@type": "PropertyValue", + "name": "item_type", + "value": "p" + }, + { + "@type": "PropertyValue", + "name": "selling_band_id", + "value": 1252439829 + }, + { + "@type": "PropertyValue", + "name": "type_name", + "value": "T-Shirt/Apparel" + }, + { + "@type": "PropertyValue", + "name": "image_ids", + "value": [ + 25073641 + ] + }, + { + "@type": "PropertyValue", + "name": "type_id", + "value": 11 + } + ], + "description": "THIS IS A PRE-ORDER\nSHIPPING AT BEGIN OF SEPTEMBER\n\ndesigned by Giambrone Studio\n80% cotton, 20% polyester (280 g/m²)\nhandmade serigraphy in Barcelona\n\nFitting examples (size: Large)\nLorenzo (180cm, 65kg)\nMollie (170cm, 50kg)\n\n*wash inside out at 30°\n\n** If the product (or size) wanted is not available here, you might want to check also on www.hex-clothing.com **", + "image": [ + "https://f4.bcbits.com/img/0025073641_10.jpg" + ], + "name": "HEX008 Hoodie", + "offers": { + "@type": "Offer", + "availability": "InStock", + "price": 49.0, + "priceCurrency": "EUR", + "priceSpecification": { + "minPrice": 49.0 + }, + "url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists#p3583995757-buy" + } + }, + { + "@id": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists#b59258785", + "@type": [ + "MusicRelease", + "Product" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "item_id", + "value": 59258785 + }, + { + "@type": "PropertyValue", + "name": "item_type", + "value": "b" + }, + { + "@type": "PropertyValue", + "name": "selling_band_id", + "value": 1252439829 + }, + { + "@type": "PropertyValue", + "name": "type_name", + "value": "Digital" + }, + { + "@type": "PropertyValue", + "name": "art_id", + "value": 464287237 + }, + { + "@type": "PropertyValue", + "name": "is_bfd", + "value": true + } + ], + "image": [ + "https://f4.bcbits.com/img/a0464287237_10.jpg" + ], + "musicReleaseFormat": "DigitalFormat", + "name": "full digital discography (48 releases)", + "offers": { + "@type": "Offer", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "bundle_size", + "value": 48 + }, + { + "@type": "PropertyValue", + "name": "discount", + "value": 0.5 + } + ], + "availability": "OnlineOnly", + "price": 154.7, + "priceCurrency": "EUR", + "priceSpecification": { + "minPrice": 154.7 + }, + "url": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists#b59258785-buy" + } + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/a1-rebekah-you-be-the-leader", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/a2-paolo-ferrara-lorenzo-raganzini-sanitary-dictature", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/a3-aeit-12-gauge", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/b1-vii-circle-demons", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/b2-rommek-mezcal-worm", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/c1-cleric-the-puppet-master", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/c2-remco-beekwilder-air-of-the-90s", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/d1-and-fearless", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/d2-benjamin-damage-matriachy", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/e1-under-black-helmet-the-purist", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/e2-maere-6siss-organisms", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/f1-imperial-black-unit-broken-tools", + "@type": "MusicRelease" + }, + { + "@id": "https://hexbarcelona.bandcamp.com/track/f2-end-train-ctrl-alt-delete-the-system", + "@type": "MusicRelease" + } + ], + "albumReleaseType": "AlbumRelease", + "byArtist": { + "@type": "MusicGroup", + "name": "Rebekah, Cleric, Under Black Helmet, Paolo Ferrara, Lorenzo Raganzini..." + }, + "copyrightNotice": "All Rights Reserved", + "dateModified": "19 Nov 2022 22:39:37 GMT", + "datePublished": "01 Sep 2021 00:00:00 GMT", + "description": "HEX008 | Angels From Hell (Various Artists)\r\nTriple-Red-Vinyl\r\n\r\n---\r\n\r\nSome feedback on the release:\r\n\r\n► Quail - HUGE pack, absolute killers all round. Thanks!\r\n► Essan - Thank you very much for sending me your promo, the tracks sound amazing, I love!!!!\r\n► JC Laurent - Super Solid VA! Thanks a lot :) Rommek & Cleric are my favs\r\n► boyd schidt - sick release thanks for the promo\r\n► Downwell - Great tracks ! Fav: Imperial Black Unit - Broken Tools\r\n► Jay Clarke - Heavy Heavy selection of tunes here. Solid release chaps! Thx for sharing!\r\n► takaaki itoh - great v.a!\r\n► slam - nice collection\r\n► Henning Baer - Massive!\r\n► Cristian Varela - AWESOME!\r\n► Roll Dann - Crazy Tracks, muchas gracias!\r\n► Luca Agnelli - super pack\r\n► ENDLEC - Superb selections as usual!\r\n► Jerm - Cleric's & Benjamin's my top picks, great compilation overall guys :)\r\n► Tom Page - Loads here for me, fantastic compilation! Love the Rommek, Benjamin Damage, Cleric & Imperial Black Unit tracks, but its all top notch, thanks :)\r\n► Alex Guerra - Brutal,Friends! Congrats for 2nd Anniversary.Thanks again!\r\n\r\n---\r\n\r\nWe're very excited to celebrate the 2nd Anniversary of \"HEX Recordings\" so for the occasion, we decided to create the most powerful release so far, a Triple-Vinyl Various Artists, which includes the creations of some of the artists that are closest to the Movement but never released on the label until now like Rebekah, Cleric, Remco Beekwilder, together with new members like Benjamin Damage, AEIT, Imperial Black Unit, End Train, Rommek, Maere, 6Siss and of course some of the ambassadors of the label like Under Black Helmet, VII Circle and the two HEX Founders Paolo Ferrara and Lorenzo Raganzini.\r\n\r\nThe title of the release is \"Angels From Hell\" and it represents the most ambitious release ever created by HEX. 3 flaming Red colour Vinyl, together with some new T-shirts + Hoodie characterised by the VA's unique design.\r\n\r\n15 Artists will represent the unity under Techno of 7 different countries: France, England, Italy, Netherlands, Lithuania, Belgium and Israel.\r\n\r\n---\r\n\r\nVinyl 1\r\nA1 - Rebekah - You Be The Leader\r\nA2 - Paolo Ferrara & Lorenzo Raganzini - Sanitary Dictature\r\nA3 - AEIT - 12 Gauge\r\nB1 - VII Circle - Demons\r\nB2 - Rommek - Mezcal Worm\r\n\r\n\r\nVinyl 2\r\nC1 - Cleric - The Puppet Master \r\nC2 - Remco Beekwilder - Air Of The 90s\r\nD1 - AnD - Fearless\r\nD2 - Benjamin Damage - Matriachy\r\n\r\n\r\nVinyl 3 \r\nE1 - Under Black Helmet - The Purist\r\nE2 - MAERE & 6SISS - Organisms\r\nF1 - Imperial Black Unit - Broken Tools\r\nF2 - End Train - Ctrl+Alt+Delete The System\r\n\r\n\r\nMasters by Alain Paul (Germany)\r\nDesign by Giambrone Studio", + "image": "https://f4.bcbits.com/img/a1834646812_10.jpg", + "keywords": [ + "Electronic", + "Hard Techno", + "Techno", + "Techno.", + "techno", + "Barcelona" + ], + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/album/hex008-angels-from-hell-various-artists", + "name": "HEX008 | Angels From Hell (Various Artists)", + "numTracks": 13, + "publisher": { + "@id": "https://hexbarcelona.bandcamp.com", + "@type": "MusicGroup", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "band_id", + "value": 1252439829 + }, + { + "@type": "PropertyValue", + "name": "has_any_downloads", + "value": true + }, + { + "@type": "PropertyValue", + "name": "has_download_codes", + "value": true + }, + { + "@type": "PropertyValue", + "name": "has_policies", + "value": true + }, + { + "@type": "PropertyValue", + "name": "image_height", + "value": 1200 + }, + { + "@type": "PropertyValue", + "name": "image_id", + "value": 23279504 + }, + { + "@type": "PropertyValue", + "name": "image_width", + "value": 1200 + } + ], + "description": "TechnoMetal label founded in 2019 by Lorenzo Raganzini and Paolo Ferrara.\n\nIt is related to HEX Techno movement.\n\nDEMOS\ndemos@hex-technomovement.com", + "foundingLocation": { + "@type": "Place", + "name": "Barcelona, Spain" + }, + "genre": "https://bandcamp.com/discover/electronic", + "image": "https://f4.bcbits.com/img/0023279504_10.jpg", + "mainEntityOfPage": [ + { + "@type": "WebSite", + "name": "hex-technomovement.com", + "url": "http://www.hex-technomovement.com" + } + ], + "name": "HEX Recordings - Techno movement", + "subjectOf": [ + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "m" + } + ], + "name": "Digital Music", + "url": "https://hexbarcelona.bandcamp.com/music" + }, + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "p" + } + ], + "name": "Vinyl Store & Merch", + "url": "https://hexbarcelona.bandcamp.com/merch" + }, + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "v" + } + ], + "name": "Videoclips", + "url": "https://hexbarcelona.bandcamp.com/video" + }, + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "c" + } + ], + "name": "Community", + "url": "https://hexbarcelona.bandcamp.com/community" + } + ] + }, + "track": { + "@type": "ItemList", + "itemListElement": [ + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/a1-rebekah-you-be-the-leader", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 1213968039 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H05M02S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/a1-rebekah-you-be-the-leader", + "name": "A1 - Rebekah - You Be The Leader" + }, + "position": 1 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/a2-paolo-ferrara-lorenzo-raganzini-sanitary-dictature", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 1087381028 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M37S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/a2-paolo-ferrara-lorenzo-raganzini-sanitary-dictature", + "name": "A2 - Paolo Ferrara & Lorenzo Raganzini - Sanitary Dictature" + }, + "position": 2 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/a3-aeit-12-gauge", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 3576637059 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H05M07S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/a3-aeit-12-gauge", + "name": "A3 - AEIT - 12 Gauge" + }, + "position": 3 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/b1-vii-circle-demons", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 2989732089 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M53S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/b1-vii-circle-demons", + "name": "B1 - VII Circle - Demons" + }, + "position": 4 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/b2-rommek-mezcal-worm", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 4029467628 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M37S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/b2-rommek-mezcal-worm", + "name": "B2 - Rommek - Mezcal Worm" + }, + "position": 5 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/c1-cleric-the-puppet-master", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 4186309778 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M07S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/c1-cleric-the-puppet-master", + "name": "C1 - Cleric - The Puppet Master " + }, + "position": 6 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/c2-remco-beekwilder-air-of-the-90s", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 1985091181 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M39S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/c2-remco-beekwilder-air-of-the-90s", + "name": "C2 - Remco Beekwilder - Air Of The 90s" + }, + "position": 7 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/d1-and-fearless", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 2851611795 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H07M16S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/d1-and-fearless", + "name": "D1 - AnD - Fearless" + }, + "position": 8 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/d2-benjamin-damage-matriachy", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 3716620399 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M27S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/d2-benjamin-damage-matriachy", + "name": "D2 - Benjamin Damage - Matriachy" + }, + "position": 9 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/e1-under-black-helmet-the-purist", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 2076756257 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H05M05S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/e1-under-black-helmet-the-purist", + "name": "E1 - Under Black Helmet - The Purist" + }, + "position": 10 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/e2-maere-6siss-organisms", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 1307806951 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M16S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/e2-maere-6siss-organisms", + "name": "E2 - MAERE & 6SISS - Organisms" + }, + "position": 11 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/f1-imperial-black-unit-broken-tools", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 1872080121 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M03S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/f1-imperial-black-unit-broken-tools", + "name": "F1 - Imperial Black Unit - Broken Tools" + }, + "position": 12 + }, + { + "@type": "ListItem", + "item": { + "@id": "https://hexbarcelona.bandcamp.com/track/f2-end-train-ctrl-alt-delete-the-system", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 2196935868 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M27S", + "mainEntityOfPage": "https://hexbarcelona.bandcamp.com/track/f2-end-train-ctrl-alt-delete-the-system", + "name": "F2 - End Train - Ctrl+Alt+Delete The System" + }, + "position": 13 + } + ], + "numberOfItems": 13 + } +} diff --git a/tests/json/media_with_track_alts_in_desc.json b/tests/json/media_with_track_alts_in_desc.json index 795d2a9..faebbe1 100644 --- a/tests/json/media_with_track_alts_in_desc.json +++ b/tests/json/media_with_track_alts_in_desc.json @@ -134,10 +134,10 @@ } ], "availability": "InStock", - "price": 28, + "price": 28.0, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 28 + "minPrice": 28.0 }, "url": "https://dominikeulberg.bandcamp.com/album/mannigfaltig#p3952929687-buy" } @@ -203,10 +203,10 @@ } ], "availability": "InStock", - "price": 12, + "price": 12.0, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 12 + "minPrice": 12.0 }, "url": "https://dominikeulberg.bandcamp.com/album/mannigfaltig#p2900502284-buy" } @@ -327,52 +327,85 @@ "@type": "MusicEvent", "location": { "@type": "MusicVenue", - "address": "Villiersdorp, South Africa", - "name": "Elandskloof Farm" + "address": "Berlin, Germany", + "name": "Ritter Butzke" }, - "name": "Elandskloof Farm, Villiersdorp, South Africa", - "startDate": "10 Mar 2023 00:00:00 GMT", - "url": "https://www.songkick.com/festivals/91431-timeless/id/40573162-timeless-festival-2023?utm_source=1471&utm_medium=partner" + "name": "Ritter Butzke, Berlin, Germany", + "startDate": "27 Apr 2024 00:00:00 GMT", + "url": "https://www.songkick.com/concerts/41600025-dominik-eulberg-at-ritter-butzke?utm_source=1471&utm_medium=partner" }, { "@type": "MusicEvent", "location": { "@type": "MusicVenue", - "address": "Berlin, Germany", - "name": "Ritter Butzke" + "address": "Memmingen, Germany", + "name": "Militärflugplatz" }, - "name": "Ritter Butzke, Berlin, Germany", - "startDate": "29 Apr 2023 00:00:00 GMT", - "url": "https://www.songkick.com/concerts/40812733-dominik-eulberg-at-ritter-butzke?utm_source=1471&utm_medium=partner" + "name": "Militärflugplatz, Memmingen, Germany", + "startDate": "17 May 2024 00:00:00 GMT", + "url": "https://www.songkick.com/festivals/1319688-ikarus/id/41573733-ikarus-festival-2024?utm_source=1471&utm_medium=partner" }, { "@type": "MusicEvent", "location": { "@type": "MusicVenue", - "address": "Amsterdam, Netherlands", - "name": "Komm schon Alter Festival" + "address": "Dresden, Germany", + "name": "Puschkin Club" }, - "name": "Komm schon Alter Festival, Amsterdam, Netherlands", - "startDate": "27 May 2023 00:00:00 GMT", - "url": "https://www.songkick.com/festivals/2357334-komm-schon-alter/id/40780548-komm-schon-alter-festival-2023?utm_source=1471&utm_medium=partner" + "name": "Puschkin Club, Dresden, Germany", + "startDate": "19 May 2024 00:00:00 GMT", + "url": "https://www.songkick.com/concerts/41896607-miura-at-puschkin-club?utm_source=1471&utm_medium=partner" }, { "@type": "MusicEvent", "location": { "@type": "MusicVenue", - "address": "Berlin, Germany", - "name": "Ritter Butzke" + "address": "Würzburg, Germany", + "name": "exit:steinburg" }, - "name": "Ritter Butzke, Berlin, Germany", - "startDate": "07 Aug 2023 00:00:00 GMT", - "url": "https://www.songkick.com/concerts/40813746-dominik-eulberg-at-ritter-butzke?utm_source=1471&utm_medium=partner" + "name": "exit:steinburg, Würzburg, Germany", + "startDate": "26 May 2024 00:00:00 GMT", + "url": "https://www.songkick.com/festivals/3653886-exitsteinburg/id/41822978-exitsteinburg-2024?utm_source=1471&utm_medium=partner" + }, + { + "@type": "MusicEvent", + "location": { + "@type": "MusicVenue", + "address": "Munich, Germany", + "name": "Olympia Reitanlage Riem" + }, + "name": "Olympia Reitanlage Riem, Munich, Germany", + "startDate": "15 Jun 2024 00:00:00 GMT", + "url": "https://www.songkick.com/festivals/476694-isle-of-summer/id/41670121-isle-of-summer-2024?utm_source=1471&utm_medium=partner" + }, + { + "@type": "MusicEvent", + "location": { + "@type": "MusicVenue", + "address": "Hamburg, Germany", + "name": "Ms Dockville Gelände" + }, + "name": "Ms Dockville Gelände, Hamburg, Germany", + "startDate": "16 Aug 2024 00:00:00 GMT", + "url": "https://www.songkick.com/festivals/222781-ms-dockville/id/41563495-ms-dockville-2024?utm_source=1471&utm_medium=partner" + }, + { + "@type": "MusicEvent", + "location": { + "@type": "MusicVenue", + "address": "Bad Aibling, Germany", + "name": "Echelon Festival" + }, + "name": "Echelon Festival, Bad Aibling, Germany", + "startDate": "16 Aug 2024 00:00:00 GMT", + "url": "https://www.songkick.com/festivals/1623139-echelon/id/41645400-echelon-festival-2024?utm_source=1471&utm_medium=partner" } ], "foundingLocation": { "@type": "Place", "name": "Montabaur, Germany" }, - "genre": "https://bandcamp.com/tag/electronic", + "genre": "https://bandcamp.com/discover/electronic", "image": "https://f4.bcbits.com/img/0014226291_10.jpg", "name": "Dominik Eulberg", "subjectOf": [ diff --git a/tests/json/remix_artists.json b/tests/json/remix_artists.json index af780ab..dd4f1ca 100644 --- a/tests/json/remix_artists.json +++ b/tests/json/remix_artists.json @@ -61,7 +61,7 @@ "name": "UNREALNUMBERS - Unseen EP (Varya Karpova & Lacchesi Remixes)" }, { - "@id": "https://maisoncloserecords.bandcamp.com/album/unrealnumbers-unseen-ep-varya-karpova-lacchesi-remixes#b51549529", + "@id": "https://maisoncloserecords.bandcamp.com/album/unrealnumbers-unseen-ep-varya-karpova-lacchesi-remixes#b58770622", "@type": [ "MusicRelease", "Product" @@ -70,7 +70,7 @@ { "@type": "PropertyValue", "name": "item_id", - "value": 51549529 + "value": 58770622 }, { "@type": "PropertyValue", @@ -90,7 +90,7 @@ { "@type": "PropertyValue", "name": "art_id", - "value": 1677076767 + "value": 1045504419 }, { "@type": "PropertyValue", @@ -99,17 +99,17 @@ } ], "image": [ - "https://f4.bcbits.com/img/a1677076767_10.jpg" + "https://f4.bcbits.com/img/a1045504419_10.jpg" ], "musicReleaseFormat": "DigitalFormat", - "name": "full digital discography (14 releases)", + "name": "full digital discography (21 releases)", "offers": { "@type": "Offer", "additionalProperty": [ { "@type": "PropertyValue", "name": "bundle_size", - "value": 14 + "value": 21 }, { "@type": "PropertyValue", @@ -118,12 +118,12 @@ } ], "availability": "OnlineOnly", - "price": 72.6, + "price": 106.2, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 72.6 + "minPrice": 106.2 }, - "url": "https://maisoncloserecords.bandcamp.com/album/unrealnumbers-unseen-ep-varya-karpova-lacchesi-remixes#b51549529-buy" + "url": "https://maisoncloserecords.bandcamp.com/album/unrealnumbers-unseen-ep-varya-karpova-lacchesi-remixes#b58770622-buy" } }, { @@ -187,25 +187,25 @@ { "@type": "PropertyValue", "name": "image_height", - "value": 2918 + "value": 2000 }, { "@type": "PropertyValue", "name": "image_id", - "value": 28195638 + "value": 31964905 }, { "@type": "PropertyValue", "name": "image_width", - "value": 2917 + "value": 2000 } ], "foundingLocation": { "@type": "Place", "name": "Paris, France" }, - "genre": "https://bandcamp.com/tag/electronic", - "image": "https://f4.bcbits.com/img/0028195638_10.jpg", + "genre": "https://bandcamp.com/discover/electronic", + "image": "https://f4.bcbits.com/img/0031964905_10.jpg", "mainEntityOfPage": [ { "@type": "WebPage", diff --git a/tests/json/remix_without_brackets.json b/tests/json/remix_without_brackets.json index d20ab08..5f06b70 100644 --- a/tests/json/remix_without_brackets.json +++ b/tests/json/remix_without_brackets.json @@ -61,7 +61,7 @@ "name": "Aluphobia x Astatine - Problem Child EP" }, { - "@id": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep#b51768982", + "@id": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep#b57237545", "@type": [ "MusicRelease", "Product" @@ -70,7 +70,7 @@ { "@type": "PropertyValue", "name": "item_id", - "value": 51768982 + "value": 57237545 }, { "@type": "PropertyValue", @@ -90,7 +90,7 @@ { "@type": "PropertyValue", "name": "art_id", - "value": 3978758765 + "value": 3901033007 }, { "@type": "PropertyValue", @@ -99,17 +99,17 @@ } ], "image": [ - "https://f4.bcbits.com/img/a3978758765_10.jpg" + "https://f4.bcbits.com/img/a3901033007_10.jpg" ], "musicReleaseFormat": "DigitalFormat", - "name": "full digital discography (56 releases)", + "name": "full digital discography (63 releases)", "offers": { "@type": "Offer", "additionalProperty": [ { "@type": "PropertyValue", "name": "bundle_size", - "value": 56 + "value": 63 }, { "@type": "PropertyValue", @@ -118,12 +118,12 @@ } ], "availability": "OnlineOnly", - "price": 66, + "price": 74.4, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 66 + "minPrice": 74.4 }, - "url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep#b51768982-buy" + "url": "https://purehate000.bandcamp.com/album/aluphobia-x-astatine-problem-child-ep#b57237545-buy" } }, { @@ -194,7 +194,7 @@ "@type": "Place", "name": "Budapest, Hungary" }, - "genre": "https://bandcamp.com/tag/electronic", + "genre": "https://bandcamp.com/discover/electronic", "image": "https://f4.bcbits.com/img/0020591482_10.jpg", "mainEntityOfPage": [ { diff --git a/tests/json/rr2.json b/tests/json/rr2.json new file mode 100644 index 0000000..8e54bec --- /dev/null +++ b/tests/json/rr2.json @@ -0,0 +1,205 @@ +{ + "@context": "https://schema.org", + "@id": "https://44labelgroup.bandcamp.com/album/rr2", + "@type": "MusicAlbum", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "art_id", + "value": 4051683054 + }, + { + "@type": "PropertyValue", + "name": "featured_track_num", + "value": 1 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "albumRelease": [ + { + "@id": "https://44labelgroup.bandcamp.com/album/rr2", + "@type": [ + "MusicRelease", + "Product" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "item_id", + "value": 1512284701 + }, + { + "@type": "PropertyValue", + "name": "item_type", + "value": "a" + }, + { + "@type": "PropertyValue", + "name": "selling_band_id", + "value": 855335734 + }, + { + "@type": "PropertyValue", + "name": "type_name", + "value": "Digital" + }, + { + "@type": "PropertyValue", + "name": "art_id", + "value": 4051683054 + } + ], + "description": "Includes high-quality download in MP3, FLAC and more. Paying supporters also get unlimited streaming via the free Bandcamp app.", + "image": [ + "https://f4.bcbits.com/img/a4051683054_10.jpg" + ], + "musicReleaseFormat": "DigitalFormat", + "name": "RR2" + }, + { + "@id": "https://44labelgroup.bandcamp.com/track/here-comes-the-storm-kobosil-44-terror-mix", + "@type": "MusicRelease" + } + ], + "albumReleaseType": "SingleRelease", + "byArtist": { + "@type": "MusicGroup", + "name": "RADICAL G & THE HORRORIST // KOBOSIL" + }, + "copyrightNotice": "All Rights Reserved", + "dateModified": "13 May 2019 20:40:29 GMT", + "datePublished": "28 Jan 2019 00:00:00 GMT", + "image": "https://f4.bcbits.com/img/a4051683054_10.jpg", + "keywords": [ + "Berlin" + ], + "mainEntityOfPage": "https://44labelgroup.bandcamp.com/album/rr2", + "name": "RR2", + "numTracks": 1, + "publisher": { + "@id": "https://44labelgroup.bandcamp.com", + "@type": "MusicGroup", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "band_id", + "value": 855335734 + }, + { + "@type": "PropertyValue", + "name": "has_any_downloads", + "value": true + }, + { + "@type": "PropertyValue", + "name": "has_download_codes", + "value": true + }, + { + "@type": "PropertyValue", + "name": "has_policies", + "value": true + }, + { + "@type": "PropertyValue", + "name": "image_height", + "value": 1500 + }, + { + "@type": "PropertyValue", + "name": "image_id", + "value": 35240397 + }, + { + "@type": "PropertyValue", + "name": "image_width", + "value": 1500 + } + ], + "foundingLocation": { + "@type": "Place", + "name": "Berlin, Germany" + }, + "image": "https://f4.bcbits.com/img/0035240397_10.jpg", + "mainEntityOfPage": [ + { + "@type": "WebSite", + "name": "r-label.group", + "url": "http://r-label.group" + } + ], + "name": "44 LABEL GROUP", + "subjectOf": [ + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "a" + } + ], + "name": "// 44 //", + "url": "https://44labelgroup.bandcamp.com/artists" + }, + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "m" + } + ], + "name": "// MUSIC //", + "url": "https://44labelgroup.bandcamp.com/music" + }, + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "c" + } + ], + "name": "community", + "url": "https://44labelgroup.bandcamp.com/community" + } + ] + }, + "track": { + "@type": "ItemList", + "itemListElement": [ + { + "@type": "ListItem", + "item": { + "@id": "https://44labelgroup.bandcamp.com/track/here-comes-the-storm-kobosil-44-terror-mix", + "@type": "MusicRecording", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "track_id", + "value": 551832148 + }, + { + "@type": "PropertyValue", + "name": "license_name", + "value": "all_rights_reserved" + } + ], + "copyrightNotice": "All Rights Reserved", + "duration": "P00H06M10S", + "mainEntityOfPage": "https://44labelgroup.bandcamp.com/track/here-comes-the-storm-kobosil-44-terror-mix", + "name": "Here Comes The Storm (Kobosil 44 Terror Mix)" + }, + "position": 1 + } + ], + "numberOfItems": 1 + } +} diff --git a/tests/json/single_only_track_name.json b/tests/json/single_only_track_name.json index 184212a..348b104 100644 --- a/tests/json/single_only_track_name.json +++ b/tests/json/single_only_track_name.json @@ -11,7 +11,7 @@ { "@type": "PropertyValue", "name": "art_id", - "value": 2158518281 + "value": 3989527092 }, { "@type": "PropertyValue", @@ -25,10 +25,11 @@ "name": "GUTKEIN" }, "copyrightNotice": "All Rights Reserved", - "dateModified": "11 Sep 2022 04:14:58 GMT", + "dateModified": "04 Mar 2023 01:32:43 GMT", "datePublished": "10 Jan 2021 00:00:00 GMT", + "description": "little distorted", "duration": "P00H05M55S", - "image": "https://f4.bcbits.com/img/a2158518281_10.jpg", + "image": "https://f4.bcbits.com/img/a3989527092_10.jpg", "inAlbum": { "@type": "MusicAlbum", "albumRelease": [ @@ -62,29 +63,95 @@ { "@type": "PropertyValue", "name": "art_id", - "value": 2158518281 + "value": 3989527092 } ], "description": "Includes high-quality download in MP3, FLAC and more. Paying supporters also get unlimited streaming via the free Bandcamp app.", "image": [ - "https://f4.bcbits.com/img/a2158518281_10.jpg" + "https://f4.bcbits.com/img/a3989527092_10.jpg" ], "musicReleaseFormat": "DigitalFormat", - "name": "oenera", + "name": "OENERA", "offers": { "@type": "Offer", "availability": "OnlineOnly", - "price": 1, + "price": 1.0, "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 1 + "minPrice": 1.0 }, "url": "https://gutkeinforu.bandcamp.com/track/oenera#t793629957-buy" } + }, + { + "@id": "https://gutkeinforu.bandcamp.com/track/oenera#b58688871", + "@type": [ + "MusicRelease", + "Product" + ], + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "item_id", + "value": 58688871 + }, + { + "@type": "PropertyValue", + "name": "item_type", + "value": "b" + }, + { + "@type": "PropertyValue", + "name": "selling_band_id", + "value": 1309392494 + }, + { + "@type": "PropertyValue", + "name": "type_name", + "value": "Digital" + }, + { + "@type": "PropertyValue", + "name": "art_id", + "value": 1922551329 + }, + { + "@type": "PropertyValue", + "name": "is_bfd", + "value": true + } + ], + "image": [ + "https://f4.bcbits.com/img/a1922551329_10.jpg" + ], + "musicReleaseFormat": "DigitalFormat", + "name": "full digital discography (41 releases)", + "offers": { + "@type": "Offer", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "bundle_size", + "value": 41 + }, + { + "@type": "PropertyValue", + "name": "discount", + "value": 0.35 + } + ], + "availability": "OnlineOnly", + "price": 54.64, + "priceCurrency": "EUR", + "priceSpecification": { + "minPrice": 54.64 + }, + "url": "https://gutkeinforu.bandcamp.com/track/oenera#b58688871-buy" + } } ], "albumReleaseType": "SingleRelease", - "name": "oenera", + "name": "OENERA", "numTracks": 1 }, "keywords": [ @@ -95,7 +162,7 @@ "trance" ], "mainEntityOfPage": "https://gutkeinforu.bandcamp.com/track/oenera", - "name": "oenera", + "name": "OENERA", "publisher": { "@id": "https://gutkeinforu.bandcamp.com", "@type": "MusicGroup", @@ -110,25 +177,30 @@ "name": "has_any_downloads", "value": true }, + { + "@type": "PropertyValue", + "name": "has_download_codes", + "value": true + }, { "@type": "PropertyValue", "name": "image_height", - "value": 3158 + "value": 1364 }, { "@type": "PropertyValue", "name": "image_id", - "value": 31082756 + "value": 33148176 }, { "@type": "PropertyValue", "name": "image_width", - "value": 3158 + "value": 1365 } ], "description": "support a tiny acquarium on the tip of a K-hall ~)", - "genre": "https://bandcamp.com/tag/electronic", - "image": "https://f4.bcbits.com/img/0031082756_10.jpg", + "genre": "https://bandcamp.com/discover/electronic", + "image": "https://f4.bcbits.com/img/0033148176_10.jpg", "mainEntityOfPage": [ { "@type": "WebPage", @@ -154,6 +226,18 @@ ], "name": "music", "url": "https://gutkeinforu.bandcamp.com/music" + }, + { + "@type": "WebPage", + "additionalProperty": [ + { + "@type": "PropertyValue", + "name": "nav_type", + "value": "c" + } + ], + "name": "community", + "url": "https://gutkeinforu.bandcamp.com/community" } ] } diff --git a/tests/json/single_track_release.json b/tests/json/single_track_release.json index 3954821..da8d474 100644 --- a/tests/json/single_track_release.json +++ b/tests/json/single_track_release.json @@ -24,7 +24,7 @@ "name": "Matriark" }, "copyrightNotice": "All Rights Reserved", - "dateModified": "05 Feb 2023 11:59:40 GMT", + "dateModified": "01 Jun 2023 17:03:15 GMT", "datePublished": "09 Nov 2020 00:00:00 GMT", "description": "mt004\n\nmastering by alexander salomonsen\n\n\nI'm very happy to announce Andrea's debut track. <3\n\nhttps://soundcloud.com/matriarkcph\nhttps://www.instagram.com/andreaapettersen/", "duration": "P00H07M01S", @@ -74,10 +74,10 @@ "offers": { "@type": "Offer", "availability": "OnlineOnly", - "price": 7.85, - "priceCurrency": "DKK", + "price": 2.0, + "priceCurrency": "EUR", "priceSpecification": { - "minPrice": 7.85 + "minPrice": 2.0 }, "url": "https://mega-tech.bandcamp.com/track/arangel#t2868806546-buy" } @@ -110,6 +110,21 @@ "@type": "PropertyValue", "name": "has_policies", "value": true + }, + { + "@type": "PropertyValue", + "name": "image_height", + "value": 364 + }, + { + "@type": "PropertyValue", + "name": "image_id", + "value": 32474138 + }, + { + "@type": "PropertyValue", + "name": "image_width", + "value": 640 } ], "description": "Malmø/Cph label run by Megatech and Matriark", @@ -117,6 +132,7 @@ "@type": "Place", "name": "Malmö, Sweden" }, + "image": "https://f4.bcbits.com/img/0032474138_10.jpg", "name": "Megatech Industries", "subjectOf": [ { diff --git a/tests/json/single_with_remixes.json b/tests/json/single_with_remixes.json index 2518df7..c3f49b7 100644 --- a/tests/json/single_with_remixes.json +++ b/tests/json/single_with_remixes.json @@ -133,10 +133,10 @@ } ], "availability": "InStock", - "price": 105, + "price": 105.0, "priceCurrency": "DKK", "priceSpecification": { - "minPrice": 105 + "minPrice": 105.0 }, "url": "https://reececox.bandcamp.com/album/emotion-1-kul-r-008#p1985399228-buy" } @@ -224,7 +224,7 @@ "@type": "Place", "name": "Berlin, Germany" }, - "genre": "https://bandcamp.com/tag/electronic", + "genre": "https://bandcamp.com/discover/electronic", "image": "https://f4.bcbits.com/img/0028280113_10.jpg", "mainEntityOfPage": [ { diff --git a/tests/test_album.py b/tests/test_album.py index ca3f6fd..5519ac9 100644 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -45,10 +45,12 @@ ("Diva (Incl. some sort of Remixes)", [], "Diva"), ("HWEP010 - MEZZ - COLOR OF WAR", ["HWEP010", "MEZZ"], "COLOR OF WAR"), ("O)))Bow 1", [], "O)))Bow 1"), - ("hi'Hello", ["hi"], "'Hello"), + ("hi'Hello", ["hi"], "hi'Hello"), + ("fjern's stuff and such", [], "fjern's stuff and such"), # only remove VA if album name starts or ends with it - ("Album VA", [], "Album"), - ("VA Album", [], "Album"), + ("Album VA", [], "Album VA"), + ("VA. Album", [], "Album"), + ("VA Album", [], "VA Album"), ("Album VA001", [], "Album VA001"), ("Album VA 03", [], "Album VA 03"), # remove (weird chars too) regardless of its position if explicitly excluded @@ -58,7 +60,20 @@ ("Label-Album", [], "Label-Album"), # and remove brackets ("Album", [], "Album"), + ("Artist EP", ["Artist"], "Artist EP"), + ("Artist & Another EP", ["Artist", "Another"], "Artist & Another EP"), ], ) def test_clean_name(name, extras, expected): assert AlbumName.clean(name, extras, label="Label") == expected + + +@pytest.mark.parametrize( + ("original", "expected"), + [ + ("Self-Medicating LP - WU87d", "Self-Medicating LP"), + ("Stone Techno Series - Tetragonal EP", "Tetragonal EP"), + ], +) +def test_parse_title(original, expected): + assert AlbumName(original, "", "").from_title == expected diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 3c048b7..79394b0 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -1,4 +1,7 @@ """Module for command line functionality tests.""" + +import sys + import pytest from beetsplug.bandcamp import get_args @@ -26,11 +29,14 @@ ], ) def test_cmdline_flags(cmdline, args): - assert vars(get_args(cmdline)) == args + sys.argv = ["beetcamp", *cmdline] + assert vars(get_args()) == args -def test_help_is_shown(capsys): +def test_required_parameter(capsys): + sys.argv = ["beetcamp"] with pytest.raises(SystemExit): - get_args([]) - capture = capsys.readouterr() - assert "options:" in capture.out + get_args() + + capture = capsys.readouterr() + assert "error: one of the arguments" in capture.err diff --git a/tests/test_genre.py b/tests/test_genre.py index a1f7079..bd38bbf 100644 --- a/tests/test_genre.py +++ b/tests/test_genre.py @@ -1,6 +1,6 @@ """Tests for genre functionality.""" import pytest -from beetsplug.bandcamp._metaguru import Metaguru +from beetsplug.bandcamp.metaguru import Metaguru pytestmark = pytest.mark.parsing @@ -24,6 +24,7 @@ def test_style(json_meta, beets_config): (["hardtrance", "hard trance"], "hard trance"), (["hard trance", "trance"], "hard trance"), (["hard trance", "hardtrance"], "hard trance"), + (["alt-country"], "alt-country"), ], ) def test_genre_variations(keywords, expected, json_meta, beets_config): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e04ea61..503048a 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,6 +1,6 @@ """Module for the helpers module tests.""" import pytest -from beetsplug.bandcamp._helpers import Helpers +from beetsplug.bandcamp.helpers import Helpers pytestmark = pytest.mark.parsing @@ -54,6 +54,7 @@ ("", "", "Modularz 40", "Modularz", "Modularz 40"), ("", "", " catalogue number GOOD001 ", "", "GOOD001"), ("", "", "RD-9", "", ""), + ("The Untold Way (Dystopian LP01)", "", "", "", "Dystopian LP01"), ], ) def test_parse_catalognum(album, disctitle, description, label, expected): diff --git a/tests/test_jsons.py b/tests/test_jsons.py index 54afd48..cd0e229 100644 --- a/tests/test_jsons.py +++ b/tests/test_jsons.py @@ -1,14 +1,15 @@ """Tests that compare beetcamp outputs against expected JSON outputs.""" + from operator import itemgetter import pytest -from beetsplug.bandcamp._metaguru import NEW_BEETS, Metaguru +from beetsplug.bandcamp.metaguru import EXTENDED_FIELDS_SUPPORT, Metaguru pytestmark = pytest.mark.jsons def check(actual, expected) -> None: - if NEW_BEETS: + if EXTENDED_FIELDS_SUPPORT: assert dict(actual) == expected else: actual = vars(actual) @@ -43,6 +44,9 @@ def test_parse_single_track_release(release, beets_config): "artist_catalognum", "album_in_titles", "remix_without_brackets", + "rr2", + "hex008", + "hex002", ], indirect=["release"], ) diff --git a/tests/test_lib.py b/tests/test_lib.py index 27c6e58..e404443 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -2,25 +2,29 @@ reference JSONs. Currently they are only executed locally and are based on the maintainer's beets library. """ + import json import os -import re -from collections import Counter, defaultdict, namedtuple +from collections import Counter, defaultdict from functools import partial -from glob import glob from itertools import groupby, starmap from operator import itemgetter -from typing import Any, Dict, Iterable, List, Tuple +from pathlib import Path +from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple import pytest +from _pytest.config import Config +from _pytest.fixtures import FixtureRequest +from beets import IncludeLazyConfig +from beets.autotag.hooks import AttrDict from beetsplug.bandcamp import BandcampPlugin -from beetsplug.bandcamp._metaguru import Metaguru -from rich import print +from beetsplug.bandcamp.metaguru import Metaguru from rich.console import Group +from rich.panel import Panel from rich.traceback import install from rich_tables.utils import ( + NewTable, border_panel, - list_table, make_console, make_difftext, new_table, @@ -32,8 +36,8 @@ JSONDict = Dict[str, Any] -LIB_TESTS_DIR = "lib_tests" -JSONS_DIR = "jsons" +LIB_TESTS_DIR = Path("lib_tests") +JSONS_DIR = Path("jsons") IGNORE_FIELDS = { "bandcamp_artist_id", @@ -50,19 +54,19 @@ "times_bought", } DO_NOT_COMPARE = {"album_id", "media", "mediums", "disctitle"} +TRACK_FIELDS = ["track_alt", "artist", "title"] install(show_locals=True, extra_lines=8, width=int(os.environ.get("COLUMNS", 150))) console = make_console(stderr=True, record=True) -Oldnew = namedtuple("Oldnew", ["old", "new", "diff"]) -oldnew = defaultdict(list) -TRACK_FIELDS = ["track_alt", "artist", "title"] +class FieldDiff(NamedTuple): + old: Any + new: Any - -def album_table(**kwargs): - table = new_table(*TRACK_FIELDS, show_header=False, expand=False, highlight=False) - return simple_panel(table, **{"expand": True, "border_style": "dim cyan", **kwargs}) + @property + def diff(self) -> str: + return str(make_difftext(str(self.old), str(self.new))) albums: List[Tuple[str, str]] = [] @@ -70,68 +74,80 @@ def album_table(**kwargs): new_fails: List[Tuple[str, str]] = [] -open = partial(open, encoding="utf-8") # pylint: disable=redefined-builtin +@pytest.fixture(scope="module") +def base_dir(pytestconfig: Config) -> Path: + return LIB_TESTS_DIR / pytestconfig.getoption("base") -def _fmt_old(s: str, times: int) -> str: - return (f"{times} x " if times > 1 else "") + wrap(s, "b s red") +@pytest.fixture(scope="module") +def target_dir(pytestconfig: Config) -> Path: + target_dir = LIB_TESTS_DIR / pytestconfig.getoption("target") + target_dir.mkdir(exist_ok=True) + + return target_dir @pytest.fixture(scope="module") -def base_dir(pytestconfig): - return os.path.join(LIB_TESTS_DIR, pytestconfig.getoption("base")) +def config() -> IncludeLazyConfig: + return BandcampPlugin().config.flatten() @pytest.fixture(scope="module") -def target_dir(pytestconfig): - target = os.path.join(LIB_TESTS_DIR, pytestconfig.getoption("target")) - if not os.path.exists(target): - os.makedirs(target) - return target +def oldnew() -> Dict[str, List[FieldDiff]]: + return defaultdict(list) -@pytest.fixture(scope="module") -def config(): - yield BandcampPlugin().config.flatten() +@pytest.fixture(params=sorted(JSONS_DIR.glob("*.json")), ids=str) +def test_filepath(request: FixtureRequest) -> Path: + return request.param -@pytest.fixture(params=sorted(glob(os.path.join(JSONS_DIR, "*.json")))) -def filename(request): - return os.path.basename(request.param) +@pytest.fixture +def target_filepath(target_dir: Path, test_filepath: Path) -> Path: + return target_dir / test_filepath.name + + +def album_table(**kwargs: JSONDict) -> Panel: + table = new_table(*TRACK_FIELDS, show_header=False, expand=False, highlight=False) + return simple_panel(table, **{"expand": True, "border_style": "dim cyan", **kwargs}) + + +def _fmt_old(s: str, times: int) -> str: + return (f"{times} x " if times > 1 else "") + wrap(s, "b s red") @pytest.fixture -def base(base_dir, filename) -> JSONDict: +def base(base_dir: Path, test_filepath: Path) -> AttrDict: try: - with open(os.path.join(base_dir, filename)) as f: + with (base_dir / test_filepath.name).open() as f: return json.load(f) except FileNotFoundError: return {} @pytest.fixture -def target(target_dir, filename): +def target(target_filepath: Path) -> AttrDict: try: - with open(os.path.join(target_dir, filename)) as f: + with target_filepath.open() as f: return json.load(f) except FileNotFoundError: return {} @pytest.fixture -def guru(config, filename): - with open(os.path.join(JSONS_DIR, filename)) as f: +def guru(config: IncludeLazyConfig, test_filepath: Path) -> Metaguru: + with test_filepath.open() as f: test_data = f.read() return Metaguru.from_html(test_data, config) def escape(string: str) -> str: - return string.replace("[", r"\[") + return str(string).replace("[", r"\[") -@pytest.fixture(scope="session") -def _report(): +@pytest.fixture(scope="module") +def _report(oldnew) -> None: yield cols = [] for field in set(oldnew.keys()) - {"comments", "genre", "track_fields"}: @@ -152,32 +168,52 @@ def _report(): if cols: console.print("") - console.print(border_panel(Group(*cols))) + console.print(border_panel(Group(*cols), title="Field diffs")) fails = [(wrap(x[0], "red"), x[1]) for x in new_fails if x] fix = [(wrap(x[0], "green"), x[1]) for x in fixed if x] + tables = [("Fixed", fix), ("Failed", fails)] if albums: - _tables = [albums, fix + [""] + [""] + fails] - else: - _tables = [fix, fails] - - _tables = list(filter(None, _tables)) + tables.insert(0, ("Albums", albums)) console.print("") - console.print(new_table(rows=[[border_panel(new_table(rows=t)) for t in _tables]])) + console.print( + new_table( + rows=[ + [ + border_panel(new_table(rows=rows), title=t) + for t, rows in tables + if rows + ] + ] + ) + ) @pytest.fixture -def new(guru, filename, base, target_dir, target): - if "_track_" in filename: - new = guru.singleton - else: - new = next((a for a in guru.albums if a.media == "Vinyl"), guru.albums[0]) +def old(base: JSONDict) -> AttrDict: + for key in IGNORE_FIELDS: + base.pop(key, None) + + return base - new.catalognum = " / ".join(x.catalognum for x in guru.albums if x.catalognum) - if new not in (base, target) or not target: - with open(os.path.join(target_dir, filename), "w") as f: +@pytest.fixture +def new( + guru: Metaguru, base: AttrDict, target: AttrDict, target_filepath: Path +) -> AttrDict: + new = ( + guru.singleton + if "_track_" in target_filepath.name + else next((a for a in guru.albums if a.media == "Vinyl"), guru.albums[0]) + ) + + new.catalognum = " / ".join( + sorted({x.catalognum for x in guru.albums if x.catalognum}) + ) + + if not target or new not in (base, target): + with target_filepath.open("w") as f: json.dump(new, f, indent=2, sort_keys=True) for key in IGNORE_FIELDS: @@ -186,97 +222,106 @@ def new(guru, filename, base, target_dir, target): @pytest.fixture -def old(base: JSONDict) -> JSONDict: - for key in IGNORE_FIELDS: - base.pop(key, None) - - return base - - -@pytest.fixture -def desc(old, new): +def desc(old: AttrDict, new: AttrDict, guru: Metaguru) -> str: get_values = itemgetter(*TRACK_FIELDS) - def get_tracks(data: JSONDict) -> List[Tuple[str, str, str]]: + def get_tracks(data: JSONDict) -> List[Tuple[str, ...]]: return [tuple(get_values(t)) for t in data.get("tracks", [])] if "/album/" in new["data_url"]: old.update(albumartist=old.pop("artist", ""), tracks=get_tracks(old)) new.update(albumartist=new.pop("artist", ""), tracks=get_tracks(new)) - return new.get("albumartist", "") + " - " + new.get("album", "") + artist, title = new.get("albumartist", ""), new.get("album", "") else: - return new["artist"] + " - " + new["title"] + artist, title = new["artist"], new["title"] + + return f"{artist} - {guru.meta['name']}" @pytest.fixture -def entity_id(new: JSONDict) -> str: +def entity_id(new: AttrDict) -> str: return new["album_id"] if "/album/" in new["data_url"] else new["track_id"] -def do_field(table, key: str, before, after, cached_value=None, album_name=None): - if before == after and cached_value is None: - return None - - key_fixed = False - if before == after: - key_fixed = True - before = cached_value - - parts: List[Tuple[str, str]] = [] - if key == "tracks": - for old_track, new_track in [ - (dict(zip(TRACK_FIELDS, a)), dict(zip(TRACK_FIELDS, b))) - for a, b in zip(before, after) - ]: - field_diffs: List[str] = [] - for field in TRACK_FIELDS: - old, new = old_track[field], new_track[field] - diff = str(make_difftext(str(old), str(new))) - field_diffs.append(diff) - if old != new: - oldnew[field].append(Oldnew(old, new, diff)) - parts.append(("tracks", " | ".join(field_diffs))) - else: - old, new = str(before), str(after) - difftext = make_difftext(old, new) - parts = [(wrap(key, "b"), difftext)] - if old != new: - oldnew[key].append(Oldnew(before, after, difftext)) - - if key_fixed: - fixed.extend(parts) - return None - - table.add_rows( parts) - if cached_value is None: - new_fails.extend(parts) - else: - albums.extend(parts) +@pytest.fixture(scope="module") +def do_field(oldnew): + def do( + table: NewTable, + field: str, + before: Any, + after: Any, + cached_value: Optional[Any] = None, + ) -> None: + if before == after and cached_value is None: + return None + + key_fixed = False + if before == after: + key_fixed = True + before = cached_value + + parts: List[Tuple[str, str]] = [] + if field == "tracks": + for old_track, new_track in [ + (dict(zip(TRACK_FIELDS, a)), dict(zip(TRACK_FIELDS, b))) + for a, b in zip(before, after) + ]: + field_diffs: List[str] = [] + for tfield in TRACK_FIELDS: + old, new = old_track[tfield], new_track[tfield] + diff = FieldDiff(str(old), str(new)) + field_diffs.append(diff.diff) + if old != new: + oldnew[tfield].append(diff) + if field_diffs: + parts.append(("tracks", " | ".join(field_diffs))) + else: + diff = FieldDiff(str(before), str(after)) + parts = [(wrap(field, "b"), diff.diff)] + if diff.old != diff.new: + oldnew[field].append(diff) + + if key_fixed: + fixed.extend(parts) + return + + table.add_rows(parts) + if cached_value is None: + new_fails.extend(parts) + else: + albums.extend(parts) + + return do @pytest.fixture -def difference(old, new, guru, cache: pytest.Cache, desc, entity_id) -> bool: +def difference( + do_field: Callable, + old: AttrDict, + new: AttrDict, + cache: pytest.Cache, + desc: str, + entity_id: str, +) -> bool: table = new_table(padding=0, expand=False, collapse_padding=True) - # if "album" in new and old["album"] != new["album"]: - # new["original_album"] = new["album"] - # old["original_album"] = guru.original_album - compare_fields = (new.keys() | old.keys()) - DO_NOT_COMPARE - - compare_field = partial(do_field, table, album_name=desc) + compare_field = partial(do_field, table) fail = False for field in sorted(compare_fields): old_val, new_val = old.get(field), new.get(field) if old_val is None and new_val is None: continue + cache_key = f"{entity_id}_{field}" compare_field(field, old_val, new_val, cached_value=cache.get(cache_key, None)) if old_val != new_val: - cache.set(cache_key, new_val or "") + backup = new_val or "" fail = True else: - cache.set(cache_key, None) + backup = None + + cache.set(cache_key, backup) for lst in albums, fixed, new_fails: if lst and lst[-1]: @@ -287,7 +332,7 @@ def difference(old, new, guru, cache: pytest.Cache, desc, entity_id) -> bool: console.print( border_panel( table, - title=escape(wrap(desc, "b")), + title=desc, expand=True, subtitle=wrap(f"{entity_id} - {new['media']}", "dim"), ) @@ -298,6 +343,6 @@ def difference(old, new, guru, cache: pytest.Cache, desc, entity_id) -> bool: @pytest.mark.usefixtures("_report") -def test_file(difference): +def test_file(difference: bool) -> None: if difference: pytest.fail(pytrace=False) diff --git a/tests/test_metaguru.py b/tests/test_metaguru.py index 4af3c5d..898ee17 100644 --- a/tests/test_metaguru.py +++ b/tests/test_metaguru.py @@ -1,9 +1,10 @@ """Module the Metaguru class functionality.""" + from copy import deepcopy from datetime import date import pytest -from beetsplug.bandcamp._metaguru import Metaguru +from beetsplug.bandcamp.metaguru import Metaguru pytestmark = pytest.mark.parsing @@ -13,7 +14,7 @@ @pytest.mark.parametrize( ("descr", "disctitle", "creds", "expected"), [ - _p("", "", "", "", id="empty"), + _p("", "", "", None, id="empty"), _p("hello", "", "", "hello", id="only main desc"), _p("", "sick vinyl", "", "sick vinyl", id="only media desc"), _p("", "", "credit", "credit", id="only credits"), diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 217ff0a..5f30212 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,4 +1,5 @@ """Tests for any logic found in the main plugin module.""" + import json from itertools import zip_longest @@ -49,7 +50,7 @@ def check_album(actual, expected): def test_find_url(mb_albumid, comments, album, expected_url): """URLs in `mb_albumid` and `comments` fields must be found.""" item = Item(mb_albumid=mb_albumid, comments=comments) - assert BandcampPlugin()._find_url(item, album, "album") == expected_url + assert BandcampPlugin()._find_url_in_item(item, album, "album") == expected_url @pytest.mark.parametrize( diff --git a/tests/test_search.py b/tests/test_search.py index 03ac560..ae9ef6a 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,6 +1,6 @@ """Tests for searching functionality.""" import pytest -from beetsplug.bandcamp._search import get_matches, parse_and_sort_results +from beetsplug.bandcamp.search import get_matches, parse_and_sort_results # simplified version of the search result HTML block HTML_ITEM = """ diff --git a/tests/test_track.py b/tests/test_track.py index 5bd44c8..9ce9712 100644 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -1,28 +1,22 @@ import pytest -from beetsplug.bandcamp._tracks import Track +from beetsplug.bandcamp.track import Track @pytest.mark.parametrize( - ("name", "initial_catalognum", "expected_title", "expected_catalognum"), + ("name", "expected_title", "expected_catalognum"), [ - ("Artist - Title CAT001", "", "Title CAT001", ""), - ("Artist - Title [CAT001]", "INIT001", "Title [CAT001]", "INIT001"), - ("Artist - Title [CAT001]", "", "Title", "CAT001"), + ("Artist - Title CAT001", "Title CAT001", None), + ("Artist - Title [CAT001]", "Title", "CAT001"), ], ) def test_parse_catalognum_from_track_name( - name, initial_catalognum, expected_title, expected_catalognum, json_track + name, expected_title, expected_catalognum, json_track ): - json_track = { - **json_track["item"], - "position": json_track["position"], - "name": name, - "name_parts": {"catalognum": initial_catalognum, "clean": name}, - } + json_track = {**json_track["item"], "position": json_track["position"]} - track = Track.from_json(json_track, "-", "Label") - assert track.title == expected_title, print(track) - assert track.catalognum == expected_catalognum, print(track) + track = Track.make(json_track, name) + assert track.title == expected_title, track + assert track.catalognum == expected_catalognum, track @pytest.mark.parametrize( diff --git a/tests/test_tracks.py b/tests/test_tracks.py index b076087..10d8801 100644 --- a/tests/test_tracks.py +++ b/tests/test_tracks.py @@ -1,8 +1,10 @@ """Module for track parsing tests.""" + from operator import attrgetter import pytest -from beetsplug.bandcamp._tracks import Tracks +from beetsplug.bandcamp.track import Track +from beetsplug.bandcamp.tracks import Tracks from rich.panel import Panel from rich.table import Table from rich.text import Text @@ -15,7 +17,7 @@ def print_result(console, case, expected, result): table = Table("result", *expected.keys(), show_header=True, border_style="black") expectedrow = [] resultrow = [] - for key in expected.keys(): + for key in expected: res_color, exp_color = "dim", "dim" expectedval = expected.get(key) resultval = result.get(key) @@ -116,7 +118,7 @@ def test_parse_track_name(name, expected, json_track, json_meta, console): json_track["item"].update(name=name) json_meta.update(track={"itemListElement": [json_track]}) - fields = "track_alt", "artist", "ft", "title", "main_title" + fields = "track_alt", "artist", "ft", "title", "title_without_remix" expected = dict(zip(fields, expected)) if not expected["track_alt"]: expected["track_alt"] = None @@ -125,37 +127,3 @@ def test_parse_track_name(name, expected, json_track, json_meta, console): result_track = list(tracks)[0] result = dict(zip(fields, attrgetter(*fields)(result_track))) assert result == expected, print_result(console, name, expected, result) - - -@pytest.mark.parametrize( - ("names", "expected_names"), - [ - _p( - ["Artist - Title", "Artist - Title"], - ["Artist - Title", "Artist - Title"], - id="no-prefixes", - ), - _p( - ["Artist - 01 Title"], - ["Artist - 01 Title"], - id="only-one-track", - ), - _p( - ["Artist - 01 Title", "Artist - Title"], - ["Artist - 01 Title", "Artist - Title"], - id="some-tracks-without-prefix", - ), - _p( - ["Artist - 1 Title", "Artist - 2. Title", "03 Title"], - ["Artist - 1 Title", "Artist - 2. Title", "03 Title"], - id="prefix-needs-to-be-two-numbers", - ), - _p( - ["Artist - 01 Title", "Artist - 02. Title", "03 Title"], - ["Artist - Title", "Artist - Title", "Title"], - id="removed-prefixes", - ), - ], -) -def test_remove_number_prefix(names, expected_names): - assert Tracks.remove_number_prefix(names) == expected_names diff --git a/url2json b/url2json index dd57c6b..3074935 100755 --- a/url2json +++ b/url2json @@ -4,12 +4,12 @@ # #Parse given bandcamp url and print the resulting json. By default, #it outputs an indented/prettified json string which is used by the tests. -#Use +#Use # -#-a to show all fields, +#-a to show all fields, #-h (human) to include colors and to page it #-s to save the initial Bandcamp JSON to ./jsons folder in the repo root -#-u to update the json data in ./tests/json. +#-u to update the json data in ./tests/json. #Ensure you have curl, jq and git on your system. # # Examples @@ -26,17 +26,24 @@ ALL_FIELDS=0 HUMAN=0 UPDATE=0 +SAVE=0 -get_json () { +get_json() { jqargs=(--sort-keys) - (( ALL_FIELDS )) || jqargs+=('del(.comment, .sponsor, .albumRelease[0].offers)') + ((ALL_FIELDS)) || jqargs+=('del(.comment, .sponsor, .albumRelease[0].offers)') - curl -sL "${1//[\"\']}" | sed 's/^[^{]*//; s/[^}]*$//; s/"/"/g' | grep -E '\{.*(dateModif|action=gift)' | { - if (( SAVE )); then + curl -sL "${1//[\"\']/}" | sed 's/^[^{]*//; s/[^}]*$//; s/"/"/g' | grep -E '\{.*(dateModif|action=gift)' | { + if ((SAVE)); then url=$1 - save_path=$(dirname "$(realpath "$0")")/jsons/${1//\//_}.json - jq -cM '' > "$save_path" - elif (( HUMAN )); then + save_path=$(realpath "$0") + save_path=$(dirname "$save_path") + save_path=$save_path/_jsons/${1//\//_}.json + jq -cM >"$save_path" + [[ -s $save_path ]] || { + rm "$save_path" + exit 1 + } + elif ((HUMAN)); then jq -C "${jqargs[@]}" | less -R else jq "${jqargs[@]}" @@ -44,17 +51,17 @@ get_json () { } } -update_test_jsons () { - files=(./tests/json/{,issues/}*.json) +update_test_jsons() { + files=(./tests/json/*.json) for file in "${files[@]}"; do url=$(jq -r '.["@id"]' "$file") printf '%-50s' "$file" - get_json "$url" \ - | jq 'del( + get_json "$url" | + jq 'del( (if .track then .track.itemListElement[].item else . end) | .additionalProperty[] | select(.name | test("mp3")) - )' > "$file" + )' >"$file" if git diff --quiet "$file" &>/dev/null; then echo -e ' \e[1;32mNo changes\e[0m' else @@ -77,7 +84,7 @@ else *) url=$arg ;; esac done - if (( UPDATE )); then + if ((UPDATE)); then update_test_jsons else get_json "$url"