diff --git a/README.md b/README.md index c85562de..aed0bd16 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ For more detailed guides, check out the [documentation](https://geo-martino.gith > Libraries log info about loaded objects to the custom `STAT` level. > ```python > import logging -> from musify.shared.logger import STAT +> from musify.log import STAT +> > logging.basicConfig(format="%(message)s", level=STAT) > ``` @@ -80,7 +81,7 @@ For more detailed guides, check out the [documentation](https://geo-martino.gith > The scopes listed in this example will allow access to read your library data and write to your playlists. > See Spotify Web API documentation for more information about [scopes](https://developer.spotify.com/documentation/web-api/concepts/scopes) ```python - from musify.spotify.api import SpotifyAPI + from musify.libraries.remote.spotify.api import SpotifyAPI api = SpotifyAPI( client_id="", @@ -103,7 +104,7 @@ For more detailed guides, check out the [documentation](https://geo-martino.gith ``` 4. Create a `SpotifyLibrary` object and load your library data as follows: ```python - from musify.spotify.library import SpotifyLibrary + from musify.libraries.remote.spotify.library import SpotifyLibrary library = SpotifyLibrary(api=api) @@ -133,7 +134,7 @@ For more detailed guides, check out the [documentation](https://geo-martino.gith ``` 5. Load some Spotify objects using any of the supported identifiers as follows: ```python - from musify.spotify.object import SpotifyTrack, SpotifyAlbum, SpotifyPlaylist, SpotifyArtist + from musify.libraries.remote.spotify.object import SpotifyTrack, SpotifyAlbum, SpotifyPlaylist, SpotifyArtist # load by ID track1 = SpotifyTrack.load("6fWoFduMpBem73DMLCOh1Z", api=api) @@ -181,7 +182,7 @@ For more detailed guides, check out the [documentation](https://geo-martino.gith #### Generic local library ```python - from musify.local.library import LocalLibrary + from musify.libraries.local.library import LocalLibrary library = LocalLibrary( library_folders=["", ...], @@ -191,7 +192,7 @@ For more detailed guides, check out the [documentation](https://geo-martino.gith #### MusicBee ```python - from musify.local.library import MusicBee + from musify.libraries.local.library import MusicBee library = MusicBee(musicbee_folder="") ``` @@ -228,7 +229,8 @@ For more detailed guides, check out the [documentation](https://geo-martino.gith 4. Get a track from your library using any of the following identifiers: ```python # get a track via its title - track = library[""] # if multiple tracks have the same title, the first matching one if returned + # if multiple tracks have the same title, the first matching one if returned + track = library[""] # get a track via its path track = library[""] # must be an absolute path @@ -265,7 +267,7 @@ For more detailed guides, check out the [documentation](https://geo-martino.gith 6. Save the tags to the file: ```python - from musify.local.track.field import LocalTrackField + from musify.libraries.local.track.field import LocalTrackField # you don't have to save all the tags you just modified # select which you wish to save first like so diff --git a/README.template.md b/README.template.md index 83b93173..1a5fe148 100644 --- a/README.template.md +++ b/README.template.md @@ -59,7 +59,8 @@ For more detailed guides, check out the [documentation](https://{program_owner_u > Libraries log info about loaded objects to the custom `STAT` level. > ```python > import logging -> from musify.shared.logger import STAT +> from musify.log import STAT +> > logging.basicConfig(format="%(message)s", level=STAT) > ``` @@ -80,7 +81,7 @@ For more detailed guides, check out the [documentation](https://{program_owner_u > The scopes listed in this example will allow access to read your library data and write to your playlists. > See Spotify Web API documentation for more information about [scopes](https://developer.spotify.com/documentation/web-api/concepts/scopes) ```python - from musify.spotify.api import SpotifyAPI + from musify.libraries.remote.spotify.api import SpotifyAPI api = SpotifyAPI( client_id="", @@ -103,7 +104,7 @@ For more detailed guides, check out the [documentation](https://{program_owner_u ``` 4. Create a `SpotifyLibrary` object and load your library data as follows: ```python - from musify.spotify.library import SpotifyLibrary + from musify.libraries.remote.spotify.library import SpotifyLibrary library = SpotifyLibrary(api=api) @@ -133,7 +134,7 @@ For more detailed guides, check out the [documentation](https://{program_owner_u ``` 5. Load some Spotify objects using any of the supported identifiers as follows: ```python - from musify.spotify.object import SpotifyTrack, SpotifyAlbum, SpotifyPlaylist, SpotifyArtist + from musify.libraries.remote.spotify.object import SpotifyTrack, SpotifyAlbum, SpotifyPlaylist, SpotifyArtist # load by ID track1 = SpotifyTrack.load("6fWoFduMpBem73DMLCOh1Z", api=api) @@ -181,7 +182,7 @@ For more detailed guides, check out the [documentation](https://{program_owner_u #### Generic local library ```python - from musify.local.library import LocalLibrary + from musify.libraries.local.library import LocalLibrary library = LocalLibrary( library_folders=["", ...], @@ -191,7 +192,7 @@ For more detailed guides, check out the [documentation](https://{program_owner_u #### MusicBee ```python - from musify.local.library import MusicBee + from musify.libraries.local.library import MusicBee library = MusicBee(musicbee_folder="") ``` @@ -228,7 +229,8 @@ For more detailed guides, check out the [documentation](https://{program_owner_u 4. Get a track from your library using any of the following identifiers: ```python # get a track via its title - track = library[""] # if multiple tracks have the same title, the first matching one if returned + # if multiple tracks have the same title, the first matching one if returned + track = library[""] # get a track via its path track = library[""] # must be an absolute path @@ -265,7 +267,7 @@ For more detailed guides, check out the [documentation](https://{program_owner_u 6. Save the tags to the file: ```python - from musify.local.track.field import LocalTrackField + from musify.libraries.local.track.field import LocalTrackField # you don't have to save all the tags you just modified # select which you wish to save first like so diff --git a/docs/_howto/scripts/local.library.backup-restore.py b/docs/_howto/scripts/local.library.backup-restore.py index 7e51b69b..cb6ced0f 100644 --- a/docs/_howto/scripts/local.library.backup-restore.py +++ b/docs/_howto/scripts/local.library.backup-restore.py @@ -1,4 +1,4 @@ -from musify.local.library import LocalLibrary +from musify.libraries.local.library import LocalLibrary library = LocalLibrary() import json @@ -13,7 +13,7 @@ library.restore_tracks(tracks) -from musify.local.track.field import LocalTrackField +from musify.libraries.local.track.field import LocalTrackField tags = [ LocalTrackField.TITLE, diff --git a/docs/_howto/scripts/local.library.load.py b/docs/_howto/scripts/local.library.load.py index 57f0f2f4..92829565 100644 --- a/docs/_howto/scripts/local.library.load.py +++ b/docs/_howto/scripts/local.library.load.py @@ -1,11 +1,11 @@ -from musify.local.library import LocalLibrary +from musify.libraries.local.library import LocalLibrary library = LocalLibrary( library_folders=["", ...], playlist_folder="", ) -from musify.local.library import MusicBee +from musify.libraries.local.library import MusicBee library = MusicBee(musicbee_folder="") diff --git a/docs/_howto/scripts/local.playlist.load-save.py b/docs/_howto/scripts/local.playlist.load-save.py index 128c565d..35d33f41 100644 --- a/docs/_howto/scripts/local.playlist.load-save.py +++ b/docs/_howto/scripts/local.playlist.load-save.py @@ -1,5 +1,5 @@ -from musify.local.playlist import M3U, XAutoPF -from musify.local.track import load_track +from musify.libraries.local.playlist import M3U, XAutoPF +from musify.libraries.local.track import load_track tracks = [ load_track(""), @@ -16,11 +16,11 @@ # pretty print information about this playlist print(playlist) -from musify.local.playlist import load_playlist +from musify.libraries.local.playlist import load_playlist playlist = load_playlist("") -from musify.local.track import load_track +from musify.libraries.local.track import load_track tracks = [ load_track(""), @@ -31,11 +31,11 @@ playlist = M3U("", tracks=tracks) -from musify.shared.file import PathMapper +from musify.file.path_mapper import PathMapper playlist = M3U("", path_mapper=PathMapper()) -from musify.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler playlist = M3U("", remote_wrangler=SpotifyDataWrangler()) diff --git a/docs/_howto/scripts/local.track.load-save.py b/docs/_howto/scripts/local.track.load-save.py index 929bde7a..f506f517 100644 --- a/docs/_howto/scripts/local.track.load-save.py +++ b/docs/_howto/scripts/local.track.load-save.py @@ -1,4 +1,4 @@ -from musify.local.track import FLAC, MP3, M4A, WMA +from musify.libraries.local.track import FLAC, MP3, M4A, WMA track = FLAC("") track = MP3("") @@ -8,11 +8,11 @@ # pretty print information about this track print(track) -from musify.local.track import load_track +from musify.libraries.local.track import load_track track = load_track("") -from musify.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler track = MP3("", remote_wrangler=SpotifyDataWrangler()) @@ -39,7 +39,7 @@ results = track.save(replace=True, dry_run=False) # ...or select which tags you wish to save like so -from musify.local.track.field import LocalTrackField +from musify.libraries.local.track.field import LocalTrackField tags = [ LocalTrackField.TITLE, diff --git a/docs/_howto/scripts/remote.new-music.py b/docs/_howto/scripts/remote.new-music.py index 1ef2ff9e..2cea989f 100644 --- a/docs/_howto/scripts/remote.new-music.py +++ b/docs/_howto/scripts/remote.new-music.py @@ -1,5 +1,5 @@ -from musify.spotify.api import SpotifyAPI -from musify.spotify.library import SpotifyLibrary +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.library import SpotifyLibrary api = SpotifyAPI() library = SpotifyLibrary(api=api) @@ -23,7 +23,7 @@ def match_date(alb) -> bool: return False -from musify.shared.remote.enum import RemoteObjectType +from musify.libraries.remote.core.enum import RemoteObjectType albums = [album for artist in library.artists for album in artist.albums if match_date(album)] albums_need_extend = [album for album in albums if len(album.tracks) < album.track_total] @@ -39,7 +39,7 @@ def match_date(alb) -> bool: # log stats about the loaded artists library.log_artists() -from musify.spotify.object import SpotifyPlaylist +from musify.libraries.remote.spotify.object import SpotifyPlaylist name = "New Music Playlist" playlist = SpotifyPlaylist.create(api=api, name=name) diff --git a/docs/_howto/scripts/reports.py b/docs/_howto/scripts/reports.py index e77f26a4..a1416a48 100644 --- a/docs/_howto/scripts/reports.py +++ b/docs/_howto/scripts/reports.py @@ -1,8 +1,8 @@ -from musify.local.library import LocalLibrary +from musify.libraries.local.library import LocalLibrary local_library = LocalLibrary() -from musify.spotify.api import SpotifyAPI -from musify.spotify.library import SpotifyLibrary +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.library import SpotifyLibrary api = SpotifyAPI() remote_library = SpotifyLibrary(api=api) @@ -10,7 +10,7 @@ report_playlist_differences(source=local_library, reference=remote_library) -from musify.local.track.field import LocalTrackField +from musify.libraries.local.track.field import LocalTrackField from musify.report import report_missing_tags tags = [ diff --git a/docs/_howto/scripts/setup.py b/docs/_howto/scripts/setup.py index 7a1ecf60..5fb96048 100644 --- a/docs/_howto/scripts/setup.py +++ b/docs/_howto/scripts/setup.py @@ -1,4 +1,4 @@ import logging -from musify.shared.logger import STAT +from musify.log import STAT logging.basicConfig(format="%(message)s", level=STAT) diff --git a/docs/_howto/scripts/spotify.api.py b/docs/_howto/scripts/spotify.api.py index da38c960..f7b07a69 100644 --- a/docs/_howto/scripts/spotify.api.py +++ b/docs/_howto/scripts/spotify.api.py @@ -1,4 +1,4 @@ -from musify.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.api import SpotifyAPI api = SpotifyAPI( client_id="", @@ -19,5 +19,5 @@ # authorise the program to access your Spotify data in your web browser api.authorise() -from musify.spotify.library import SpotifyLibrary +from musify.libraries.remote.spotify.library import SpotifyLibrary library = SpotifyLibrary(api=api) diff --git a/docs/_howto/scripts/spotify.library.backup-restore.py b/docs/_howto/scripts/spotify.library.backup-restore.py index 04af123e..2e567d9b 100644 --- a/docs/_howto/scripts/spotify.library.backup-restore.py +++ b/docs/_howto/scripts/spotify.library.backup-restore.py @@ -1,5 +1,5 @@ -from musify.spotify.api import SpotifyAPI -from musify.spotify.library import SpotifyLibrary +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.library import SpotifyLibrary api = SpotifyAPI() library = SpotifyLibrary(api=api) diff --git a/docs/_howto/scripts/spotify.load.py b/docs/_howto/scripts/spotify.load.py index 1e63b0f9..e7d60cfc 100644 --- a/docs/_howto/scripts/spotify.load.py +++ b/docs/_howto/scripts/spotify.load.py @@ -1,7 +1,7 @@ -from musify.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.api import SpotifyAPI api = SpotifyAPI() -from musify.spotify.library import SpotifyLibrary +from musify.libraries.remote.spotify.library import SpotifyLibrary library = SpotifyLibrary(api=api) @@ -29,7 +29,7 @@ # pretty print an overview of your library print(library) -from musify.spotify.object import SpotifyTrack, SpotifyAlbum, SpotifyPlaylist, SpotifyArtist +from musify.libraries.remote.spotify.object import SpotifyTrack, SpotifyAlbum, SpotifyPlaylist, SpotifyArtist # load by ID track1 = SpotifyTrack.load("6fWoFduMpBem73DMLCOh1Z", api=api) diff --git a/docs/_howto/scripts/sync.py b/docs/_howto/scripts/sync.py index 6bc6125d..0c70c757 100644 --- a/docs/_howto/scripts/sync.py +++ b/docs/_howto/scripts/sync.py @@ -1,8 +1,8 @@ -from musify.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.api import SpotifyAPI api = SpotifyAPI() -from musify.local.library import LocalLibrary -from musify.spotify.processors import SpotifyDataWrangler +from musify.libraries.local.library import LocalLibrary +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler local_library = LocalLibrary( library_folders=["", ...], @@ -12,9 +12,9 @@ ) local_library.load() -from musify.shared.remote.processors.search import RemoteItemSearcher -from musify.shared.remote.processors.check import RemoteItemChecker -from musify.spotify.factory import SpotifyObjectFactory +from musify.libraries.remote.core.processors.search import RemoteItemSearcher +from musify.libraries.remote.core.processors.check import RemoteItemChecker +from musify.libraries.remote.spotify.factory import SpotifyObjectFactory albums = local_library.albums[:3] factory = SpotifyObjectFactory(api=api) @@ -25,7 +25,7 @@ checker = RemoteItemChecker(object_factory=factory) checker.check(albums) -from musify.spotify.object import SpotifyTrack +from musify.libraries.remote.spotify.object import SpotifyTrack for album in albums: for local_track in album: @@ -46,7 +46,7 @@ # ...save all tracks on the album at once here album.save_tracks(replace=True, dry_run=False) -from musify.spotify.library import SpotifyLibrary +from musify.libraries.remote.spotify.library import SpotifyLibrary remote_library = SpotifyLibrary(api=api) remote_library.load_playlists() diff --git a/docs/index.rst b/docs/index.rst index bd5fb6ca..3d12dbaf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,9 +61,15 @@ Install through pip using one of the following commands: :maxdepth: 1 :caption: 📖 Reference - musify.local + musify.api + musify.core + musify.file + musify.libraries + musify.log musify.processors - musify.shared - musify.spotify + musify.exception + musify.field musify.report + musify.types + musify.utils genindex diff --git a/docs/release-history.rst b/docs/release-history.rst index 143fd37b..ad389105 100644 --- a/docs/release-history.rst +++ b/docs/release-history.rst @@ -43,10 +43,10 @@ Added Changed ------- +* Major refactoring and restructuring to all modules to improve modularity and add composition * :py:meth:`.LocalLibrary.load_tracks` and :py:meth:`.LocalLibrary.load_playlists` now run concurrently. * Made :py:func:`.load_tracks` and :py:func:`.load_playlists` utility functions more DRY * Move :py:meth:`.TagReader.load` from :py:class:`.LocalTrack` to super class :py:class:`.TagReader` -* Major refactoring and restructuring to local and remote modules to add composition * :py:meth:`.SpotifyAPI.extend_items` now skips on responses that are already fully extended * :py:meth:`.SpotifyArtist.load` now uses the base `load` method from :py:class:`.SpotifyCollectionLoader` meaning it now takes full advantage of the item filtering this method offers. diff --git a/logging.yml b/logging.yml index d9b1f4a6..fc3c47ff 100644 --- a/logging.yml +++ b/logging.yml @@ -16,10 +16,10 @@ formatters: filters: console: - (): "musify.shared.logger.LogConsoleFilter" + (): "musify.log.filter.LogConsoleFilter" module_width: 40 file: - (): "musify.shared.logger.LogFileFilter" + (): "musify.log.filter.LogFileFilter" module_width: 40 handlers: @@ -50,7 +50,7 @@ handlers: stream: ext://sys.stdout file: - class: musify.shared.handlers.CurrentTimeRotatingFileHandler + class: musify.log.handlers.CurrentTimeRotatingFileHandler level: DEBUG formatter: extended filters: ["file"] diff --git a/musify/__init__.py b/musify/__init__.py index 33f0b564..65be9194 100644 --- a/musify/__init__.py +++ b/musify/__init__.py @@ -1,5 +1,4 @@ """Welcome to Musify""" - from os.path import basename, dirname PROGRAM_NAME = "Musify" diff --git a/musify/shared/api/__init__.py b/musify/api/__init__.py similarity index 100% rename from musify/shared/api/__init__.py rename to musify/api/__init__.py diff --git a/musify/shared/api/authorise.py b/musify/api/authorise.py similarity index 97% rename from musify/shared/api/authorise.py rename to musify/api/authorise.py index c36c0efb..3906ac86 100644 --- a/musify/shared/api/authorise.py +++ b/musify/api/authorise.py @@ -1,7 +1,6 @@ """ Handle API authorisation for requesting access tokens to an API. """ - import json import logging import os @@ -15,8 +14,8 @@ import requests from musify import PROGRAM_NAME -from musify.shared.api.exception import APIError -from musify.shared.logger import MusifyLogger +from musify.api.exception import APIError +from musify.log.logger import MusifyLogger class APIAuthoriser: diff --git a/musify/shared/api/exception.py b/musify/api/exception.py similarity index 89% rename from musify/shared/api/exception.py rename to musify/api/exception.py index 28dfd17d..d2052cf0 100644 --- a/musify/shared/api/exception.py +++ b/musify/api/exception.py @@ -1,10 +1,9 @@ """ Exceptions relating to API operations. """ - from requests import Response -from musify.shared.exception import MusifyError +from musify.exception import MusifyError class APIError(MusifyError): diff --git a/musify/shared/api/request.py b/musify/api/request.py similarity index 96% rename from musify/shared/api/request.py rename to musify/api/request.py index 14d33bc5..b4fa44df 100644 --- a/musify/shared/api/request.py +++ b/musify/api/request.py @@ -1,7 +1,6 @@ """ All operations relating to handling of requests to an API. """ - import json from collections.abc import Mapping, Iterable from datetime import datetime, timedelta @@ -13,8 +12,8 @@ from requests import Response, Session from requests_cache import CachedSession, ExpirationTime -from musify.shared.api.authorise import APIAuthoriser -from musify.shared.api.exception import APIError +from musify.api.authorise import APIAuthoriser +from musify.api.exception import APIError class RequestHandler(APIAuthoriser): diff --git a/musify/core/__init__.py b/musify/core/__init__.py new file mode 100644 index 00000000..6120d319 --- /dev/null +++ b/musify/core/__init__.py @@ -0,0 +1,3 @@ +""" +All core abstract classes for the entire package. +""" diff --git a/musify/shared/core/base.py b/musify/core/base.py similarity index 92% rename from musify/shared/core/base.py rename to musify/core/base.py index ae758458..a42cd1fc 100644 --- a/musify/shared/core/base.py +++ b/musify/core/base.py @@ -1,15 +1,14 @@ """ The fundamental core classes for the entire package. """ - from __future__ import annotations from abc import ABCMeta, abstractmethod from collections.abc import Hashable from typing import Any -from musify.shared.core.enum import TagField -from musify.shared.core.misc import AttributePrinter +from musify.core.enum import TagField +from musify.core.printer import AttributePrinter class MusifyObject(AttributePrinter): diff --git a/musify/shared/core/enum.py b/musify/core/enum.py similarity index 95% rename from musify/shared/core/enum.py rename to musify/core/enum.py index f09669f0..3548907e 100644 --- a/musify/shared/core/enum.py +++ b/musify/core/enum.py @@ -1,15 +1,14 @@ """ The fundamental core enum classes for the entire package. """ - from collections.abc import Sequence from dataclasses import dataclass, field from enum import IntEnum from typing import Self -from musify.shared.exception import MusifyEnumError -from musify.shared.types import UnitIterable -from musify.shared.utils import unique_list +from musify.exception import MusifyEnumError +from musify.types import UnitIterable +from musify.utils import unique_list class MusifyEnum(IntEnum): diff --git a/musify/shared/core/misc.py b/musify/core/printer.py similarity index 90% rename from musify/shared/core/misc.py rename to musify/core/printer.py index 90f06c42..92e5cc05 100644 --- a/musify/shared/core/misc.py +++ b/musify/core/printer.py @@ -1,24 +1,14 @@ """ -The fundamental core miscellaneous classes for the entire package. +The fundamental core printer classes for the entire package. """ - import re -from abc import ABC, ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Mapping -from dataclasses import dataclass from datetime import datetime, date from typing import Any -from musify.shared.types import UnitIterable -from musify.shared.utils import to_collection - -_T_JSON_VALUE = str | int | float | list | dict | bool | None - - -@dataclass(frozen=True) -class Result(metaclass=ABCMeta): - """Stores the results of an operation""" - pass +from musify.types import UnitIterable, JSON, DictJSON +from musify.utils import to_collection class PrettyPrinter(ABC): @@ -51,13 +41,13 @@ def as_dict(self) -> dict[str, Any]: """ raise NotImplementedError - def json(self) -> dict[str, _T_JSON_VALUE]: + def json(self) -> DictJSON: """Return a dictionary representation of the key attributes of this object that is safe to output to JSON""" return self._to_json(self.as_dict()) @classmethod - def _to_json(cls, attributes: Mapping[str, _T_JSON_VALUE]) -> dict[str, _T_JSON_VALUE]: - result: dict[str, _T_JSON_VALUE] = {} + def _to_json(cls, attributes: JSON) -> DictJSON: + result: DictJSON = {} for attr_key, attr_val in attributes.items(): attr_key = str(attr_key) diff --git a/musify/core/result.py b/musify/core/result.py new file mode 100644 index 00000000..41bdb186 --- /dev/null +++ b/musify/core/result.py @@ -0,0 +1,11 @@ +""" +The fundamental core result classes for the entire package. +""" +from abc import ABC +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Result(ABC): + """Stores the results of an operation""" + pass diff --git a/musify/exception.py b/musify/exception.py new file mode 100644 index 00000000..2de7db4c --- /dev/null +++ b/musify/exception.py @@ -0,0 +1,53 @@ +""" +Core exceptions for the entire package. +""" +from typing import Any + + +class MusifyError(Exception): + """Generic base class for all Musify-related errors""" + + +class MusifyKeyError(MusifyError, KeyError): + """Exception raised for invalid keys.""" + + +class MusifyValueError(MusifyError, ValueError): + """Exception raised for invalid values.""" + + +class MusifyTypeError(MusifyError, TypeError): + """Exception raised for invalid types.""" + def __init__(self, kind: Any, message: str = "Invalid item type given"): + self.message = message + super().__init__(f"{self.message}: {kind}") + + +class MusifyAttributeError(MusifyError, AttributeError): + """Exception raised for invalid attributes.""" + + +########################################################################### +## Enum errors +########################################################################### +class MusifyEnumError(MusifyError): + """ + Exception raised for errors related to :py:class:`MusifyEnum` implementations. + + :param value: The value that caused the error. + :param message: Explanation of the error. + """ + + def __init__(self, value: Any, message: str = "Could not find enum"): + self.message = message + super().__init__(f"{self.message}: {value}") + + +class FieldError(MusifyEnumError): + """ + Exception raised for errors related to :py:class:`Field` enums. + + :param message: Explanation of the error. + """ + def __init__(self, message: str | None = None, field: Any | None = None): + super().__init__(value=field, message=message) diff --git a/musify/shared/field.py b/musify/field.py similarity index 93% rename from musify/shared/field.py rename to musify/field.py index 644abba9..ca63dbd2 100644 --- a/musify/shared/field.py +++ b/musify/field.py @@ -1,11 +1,10 @@ """ All core :py:class:`Field` implementations relating to -core :py:class:`Item` and :py:class`ItemCollection` implementations. +core :py:class:`MusifyItem` and :py:class`MusifyCollection` implementations. """ - from typing import Self -from musify.shared.core.enum import Field, Fields, TagField, TagFields +from musify.core.enum import Field, Fields, TagField, TagFields class TrackFieldMixin(TagField): diff --git a/musify/file/__init__.py b/musify/file/__init__.py new file mode 100644 index 00000000..0cec1a65 --- /dev/null +++ b/musify/file/__init__.py @@ -0,0 +1,3 @@ +""" +All base classes relating to generic file operations. +""" diff --git a/musify/file/base.py b/musify/file/base.py new file mode 100644 index 00000000..1f74cdc9 --- /dev/null +++ b/musify/file/base.py @@ -0,0 +1,100 @@ +""" +Generic base classes and functions for file operations. +""" +from abc import ABCMeta, abstractmethod +from collections.abc import Hashable +from datetime import datetime +from glob import glob +from os.path import splitext, basename, dirname, getsize, getmtime, getctime, exists, join +from typing import Any + +from musify.file.exception import InvalidFileType, FileDoesNotExistError + + +class File(Hashable, metaclass=ABCMeta): + """Generic class for representing a file on a system.""" + + #: Extensions of files that can be loaded by this class. + valid_extensions: frozenset[str] + + @property + @abstractmethod + def path(self) -> str: + """The path to the file.""" + raise NotImplementedError + + @property + def folder(self) -> str: + """The parent folder of the file.""" + return basename(dirname(self.path)) + + @property + def filename(self) -> str: + """The filename without extension.""" + return splitext(basename(self.path))[0] + + @property + def ext(self) -> str: + """The file extension in lowercase.""" + return splitext(self.path)[1].lower() + + @property + def size(self) -> int | None: + """The size of the file in bytes""" + return getsize(self.path) if exists(self.path) else None + + @property + def date_created(self) -> datetime | None: + """:py:class:`datetime` object representing when the file was created""" + return datetime.fromtimestamp(getctime(self.path)) if exists(self.path) else None + + @property + def date_modified(self) -> datetime | None: + """:py:class:`datetime` object representing when the file was last modified""" + return datetime.fromtimestamp(getmtime(self.path)) if exists(self.path) else None + + @classmethod + def _validate_type(cls, path: str) -> None: + """Raises an exception if the ``path`` extension is not accepted""" + ext = splitext(path)[1].casefold() + if ext not in cls.valid_extensions: + raise InvalidFileType( + ext, + f"Not an accepted {cls.__name__} file extension. " + f"Use only: {', '.join(cls.valid_extensions)}" + ) + + @staticmethod + def _validate_existence(path: str): + """Raises an exception if there is no file at the given ``path``""" + if not path or not exists(path): + raise FileDoesNotExistError(f"File not found | {path}") + + @classmethod + def get_filepaths(cls, folder: str) -> set[str]: + """Get all files in a given folder that match this File object's valid filetypes recursively.""" + paths = set() + + for ext in cls.valid_extensions: + paths |= set(glob(join(folder, "**", f"*{ext}"), recursive=True, include_hidden=True)) + + # do not return paths in the recycle bin in Windows-based folders + return {path for path in paths if "$RECYCLE.BIN" not in path} + + @abstractmethod + def load(self, *args, **kwargs) -> Any: + """Load the file to this object""" + raise NotImplementedError + + @abstractmethod + def save(self, dry_run: bool = True, *args, **kwargs) -> Any: + """ + Save this object to file. + + :param dry_run: Run function, but do not modify file at all. + """ + raise NotImplementedError + + def __hash__(self): + """Uniqueness of a file is its path""" + return hash(self.path) diff --git a/musify/file/exception.py b/musify/file/exception.py new file mode 100644 index 00000000..128d7fe9 --- /dev/null +++ b/musify/file/exception.py @@ -0,0 +1,51 @@ +""" +Exceptions relating to file operations. +""" +from musify.exception import MusifyError + + +class FileError(MusifyError): + """ + Exception raised for file errors. + + :param file: The file type that caused the error. + :param message: Explanation of the error. + """ + + def __init__(self, file: str | None = None, message: str | None = None): + self.file = file + self.message = message + formatted = f"{file} | {message}" if file else message + super().__init__(formatted) + + +class InvalidFileType(FileError): + """ + Exception raised for unrecognised file types. + + :param filetype: The file type that caused the error. + :param message: Explanation of the error. + """ + + def __init__(self, filetype: str, message: str = "File type not recognised"): + self.filetype = filetype + self.message = message + super().__init__(file=filetype, message=message) + + +class FileDoesNotExistError(FileError, FileNotFoundError): + """ + Exception raised when a file cannot be found. + + :param path: The path that caused the error. + :param message: Explanation of the error. + """ + + def __init__(self, path: str, message: str = "File cannot be found"): + self.path = path + self.message = message + super().__init__(file=path, message=message) + + +class ImageLoadError(FileError): + """Exception raised for errors in loading an image.""" diff --git a/musify/shared/image.py b/musify/file/image.py similarity index 88% rename from musify/shared/image.py rename to musify/file/image.py index a62caab9..1ba39763 100644 --- a/musify/shared/image.py +++ b/musify/file/image.py @@ -1,3 +1,6 @@ +""" +Functionality relating to reading and writing images. +""" from http.client import HTTPResponse from io import BytesIO from pathlib import Path @@ -6,7 +9,7 @@ from PIL import Image, UnidentifiedImageError -from musify.shared.exception import ImageLoadError +from musify.file.exception import ImageLoadError def open_image(source: str | bytes | Path | Request) -> Image.Image: diff --git a/musify/shared/file.py b/musify/file/path_mapper.py similarity index 67% rename from musify/shared/file.py rename to musify/file/path_mapper.py index d0f733f0..dbe5c713 100644 --- a/musify/shared/file.py +++ b/musify/file/path_mapper.py @@ -1,106 +1,13 @@ """ -Generic file operations. Base class for generic File and functions for reading/writing basic file types. +Operations relating to mapping and re-mapping of paths. """ - -from abc import ABCMeta, abstractmethod -from collections.abc import Hashable, Collection, Iterable -from datetime import datetime -from glob import glob +from collections.abc import Collection, Iterable from os import sep -from os.path import splitext, basename, dirname, getsize, getmtime, getctime, exists, join +from os.path import exists from typing import Any -from musify.shared.core.misc import PrettyPrinter -from musify.shared.exception import InvalidFileType, FileDoesNotExistError - - -class File(Hashable, metaclass=ABCMeta): - """Generic class for representing a file on a system.""" - - #: Extensions of files that can be loaded by this class. - valid_extensions: frozenset[str] - - @property - @abstractmethod - def path(self) -> str: - """The path to the file.""" - raise NotImplementedError - - @property - def folder(self) -> str: - """The parent folder of the file.""" - return basename(dirname(self.path)) - - @property - def filename(self) -> str: - """The filename without extension.""" - return splitext(basename(self.path))[0] - - @property - def ext(self) -> str: - """The file extension in lowercase.""" - return splitext(self.path)[1].lower() - - @property - def size(self) -> int | None: - """The size of the file in bytes""" - return getsize(self.path) if exists(self.path) else None - - @property - def date_created(self) -> datetime | None: - """:py:class:`datetime` object representing when the file was created""" - return datetime.fromtimestamp(getctime(self.path)) if exists(self.path) else None - - @property - def date_modified(self) -> datetime | None: - """:py:class:`datetime` object representing when the file was last modified""" - return datetime.fromtimestamp(getmtime(self.path)) if exists(self.path) else None - - @classmethod - def _validate_type(cls, path: str) -> None: - """Raises an exception if the ``path`` extension is not accepted""" - ext = splitext(path)[1].casefold() - if ext not in cls.valid_extensions: - raise InvalidFileType( - ext, - f"Not an accepted {cls.__name__} file extension. " - f"Use only: {', '.join(cls.valid_extensions)}" - ) - - @staticmethod - def _validate_existence(path: str): - """Raises an exception if there is no file at the given ``path``""" - if not path or not exists(path): - raise FileDoesNotExistError(f"File not found | {path}") - - @classmethod - def get_filepaths(cls, folder: str) -> set[str]: - """Get all files in a given folder that match this File object's valid filetypes recursively.""" - paths = set() - - for ext in cls.valid_extensions: - paths |= set(glob(join(folder, "**", f"*{ext}"), recursive=True, include_hidden=True)) - - # do not return paths in the recycle bin in Windows-based folders - return {path for path in paths if "$RECYCLE.BIN" not in path} - - @abstractmethod - def load(self, *args, **kwargs) -> Any: - """Load the file to this object""" - raise NotImplementedError - - @abstractmethod - def save(self, dry_run: bool = True, *args, **kwargs) -> Any: - """ - Save this object to file. - - :param dry_run: Run function, but do not modify file at all. - """ - raise NotImplementedError - - def __hash__(self): - """Uniqueness of a file is its path""" - return hash(self.path) +from musify.core.printer import PrettyPrinter +from musify.file.base import File class PathMapper(PrettyPrinter): diff --git a/musify/libraries/__init__.py b/musify/libraries/__init__.py new file mode 100644 index 00000000..bde9b66c --- /dev/null +++ b/musify/libraries/__init__.py @@ -0,0 +1,3 @@ +""" +All modules and scripts pertaining to library operations including all local and remote libraries and their objects. +""" diff --git a/musify/libraries/collection.py b/musify/libraries/collection.py new file mode 100644 index 00000000..6a544c6c --- /dev/null +++ b/musify/libraries/collection.py @@ -0,0 +1,42 @@ +""" +Basic concrete implementation of a MusifyCollection. +""" +from __future__ import annotations + +from collections.abc import Iterable, Collection +from typing import Any + +from musify.core.base import MusifyItem +from musify.libraries.core.collection import MusifyCollection +from musify.utils import to_collection + + +class BasicCollection[T: MusifyItem](MusifyCollection[T]): + """ + A basic implementation of MusifyCollection for storing ``items`` with a given ``name``. + + :param name: The name of this collection. + :param items: The items in this collection + """ + + __slots__ = ("_name", "_items") + + @staticmethod + def _validate_item_type(items: Any | Iterable[Any]) -> bool: + if isinstance(items, Iterable): + return all(isinstance(item, MusifyItem) for item in items) + return isinstance(items, MusifyItem) + + @property + def name(self): + """The name of this collection""" + return self._name + + @property + def items(self) -> list[T]: + return self._items + + def __init__(self, name: str, items: Collection[T]): + super().__init__() + self._name = name + self._items = to_collection(items, list) diff --git a/musify/libraries/core/__init__.py b/musify/libraries/core/__init__.py new file mode 100644 index 00000000..c168936d --- /dev/null +++ b/musify/libraries/core/__init__.py @@ -0,0 +1,3 @@ +""" +Abstract base classes for all library objects. +""" diff --git a/musify/shared/core/collection.py b/musify/libraries/core/collection.py similarity index 90% rename from musify/shared/core/collection.py rename to musify/libraries/core/collection.py index 46000038..2647a05c 100644 --- a/musify/shared/core/collection.py +++ b/musify/libraries/core/collection.py @@ -1,7 +1,6 @@ """ The fundamental core collection classes for the entire package. """ - from __future__ import annotations from abc import ABCMeta, abstractmethod, ABC @@ -9,15 +8,15 @@ from dataclasses import dataclass from typing import Any, SupportsIndex, Self +from musify.core.base import MusifyObject, MusifyItem +from musify.core.enum import Field +from musify.exception import MusifyTypeError, MusifyKeyError, MusifyAttributeError +from musify.file.base import File +from musify.libraries.remote.core import RemoteResponse +from musify.libraries.remote.core.base import RemoteObject +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler from musify.processors.sort import ShuffleMode, ShuffleBy, ItemSorter -from musify.shared.core.base import MusifyObject, MusifyItem -from musify.shared.core.enum import Field -from musify.shared.exception import MusifyTypeError, MusifyKeyError, MusifyAttributeError -from musify.shared.file import File -from musify.shared.remote import RemoteResponse -from musify.shared.remote.base import RemoteObject -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.shared.types import UnitSequence +from musify.types import UnitSequence @dataclass @@ -79,7 +78,7 @@ class RemoteURIGetter(ItemGetterStrategy): def name(self) -> str: return "URI" - def get_value_from_item(self, item: MusifyItem | RemoteObject) -> str: + def get_value_from_item(self, item: MusifyItem | RemoteResponse) -> str: return item.uri @@ -114,8 +113,8 @@ def items(self) -> list[T]: @abstractmethod def _validate_item_type(items: Any | Iterable[Any]) -> bool: """ - Validate the given :py:class:`Item` by ensuring it matches the allowed item type for this collection. - Used to validate input :py:class:`Item` types given to functions that + Validate the given :py:class:`MusifyItem` by ensuring it matches the allowed item type for this collection. + Used to validate input :py:class:`MusifyItem` types given to functions that modify the stored items in this collection. :param items: The item or items to validate @@ -129,7 +128,7 @@ def __init__(self, remote_wrangler: RemoteDataWrangler | None = None): self.remote_wrangler = remote_wrangler def count(self, __item: T) -> int: - """Return the number of occurrences of the given :py:class:`Item` in this collection""" + """Return the number of occurrences of the given :py:class:`MusifyItem` in this collection""" if not self._validate_item_type(__item): raise MusifyTypeError(type(__item).__name__) return self.items.count(__item) @@ -169,7 +168,7 @@ def extend(self, __items: Iterable[T], allow_duplicates: bool = True) -> None: self.items.extend(item for item in __items if item not in self.items) def insert(self, __index: int, __item: T, allow_duplicates: bool = True) -> None: - """Insert given :py:class:`Item` before the given index""" + """Insert given :py:class:`MusifyItem` before the given index""" if not self._validate_item_type(__item): raise MusifyTypeError(type(__item)) if allow_duplicates or __item not in self.items: @@ -294,7 +293,7 @@ def __getitem__( ) -> T | list[T] | list[T, None, None]: """ Returns the item in this collection by matching on a given index/Item/URI/ID/URL. - If an :py:class:`Item` is given, the URI is extracted from this item + If an :py:class:`MusifyItem` is given, the URI is extracted from this item and the matching Item from this collection is returned. If a :py:class:`RemoteObject` is given, the ID is extracted from this object and the matching RemoteObject from this collection is returned. @@ -332,8 +331,7 @@ def __getitem__( caught_exceptions.append(ex) raise MusifyKeyError( - "Key is invalid. The following errors were thrown: \n- " - "\n- ".join([str(ex) for ex in caught_exceptions]) + f"Key is invalid. The following errors were thrown: {[str(ex) for ex in caught_exceptions]}" ) def __setitem__(self, __key: str | int | T, __value: T): diff --git a/musify/shared/core/object.py b/musify/libraries/core/object.py similarity index 89% rename from musify/shared/core/object.py rename to musify/libraries/core/object.py index 420786c3..e56f10c2 100644 --- a/musify/shared/core/object.py +++ b/musify/libraries/core/object.py @@ -1,24 +1,23 @@ """ -The core implementations of :py:class:`Item` and :py:class:`ItemCollection` classes. +The core abstract implementations of :py:class:`MusifyItem` and :py:class:`MusifyCollection` classes. """ - from __future__ import annotations import datetime import logging from abc import ABCMeta, abstractmethod -from collections.abc import Iterable, Collection, Mapping +from collections.abc import Collection, Mapping from copy import deepcopy -from typing import Any, Self +from typing import Any +from musify.core.base import MusifyItem +from musify.exception import MusifyTypeError +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.log.logger import MusifyLogger from musify.processors.base import Filter from musify.processors.filter import FilterDefinedList -from musify.shared.core.base import MusifyItem -from musify.shared.core.collection import MusifyCollection -from musify.shared.exception import MusifyTypeError -from musify.shared.logger import MusifyLogger -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.shared.utils import to_collection, align_string, get_max_width +from musify.utils import align_string, get_max_width class Track(MusifyItem, metaclass=ABCMeta): @@ -165,37 +164,6 @@ def rating(self) -> float | None: raise NotImplementedError -class BasicCollection[T: MusifyItem](MusifyCollection[T]): - """ - A basic implementation of ItemCollection for storing ``items`` with a given ``name``. - - :param name: The name of this collection. - :param items: The items in this collection - """ - - __slots__ = ("_name", "_items") - - @staticmethod - def _validate_item_type(items: Any | Iterable[Any]) -> bool: - if isinstance(items, Iterable): - return all(isinstance(item, MusifyItem) for item in items) - return isinstance(items, MusifyItem) - - @property - def name(self): - """The name of this collection""" - return self._name - - @property - def items(self) -> list[T]: - return self._items - - def __init__(self, name: str, items: Collection[T]): - super().__init__() - self._name = name - self._items = to_collection(items, list) - - class Playlist[T: Track](MusifyCollection[T], metaclass=ABCMeta): """A playlist of items and their derived properties/objects.""" @@ -269,8 +237,7 @@ def merge(self, playlist: Playlist[T]) -> None: # TODO: merge playlists adding/removing tracks as needed. raise NotImplementedError - # noinspection PyTypeChecker - def __or__(self, other: Playlist[T]) -> Self: + def __or__(self, other: Playlist[T]): if not isinstance(other, self.__class__): raise MusifyTypeError( f"Incorrect item given. Cannot merge with {other.__class__.__name__} " @@ -278,8 +245,7 @@ def __or__(self, other: Playlist[T]) -> Self: ) raise NotImplementedError - # noinspection PyTypeChecker - def __ior__(self, other: Playlist[T]) -> Self: + def __ior__(self, other: Playlist[T]): if not isinstance(other, self.__class__): raise MusifyTypeError( f"Incorrect item given. Cannot merge with {other.__class__.__name__} " diff --git a/musify/local/__init__.py b/musify/libraries/local/__init__.py similarity index 100% rename from musify/local/__init__.py rename to musify/libraries/local/__init__.py diff --git a/musify/local/base.py b/musify/libraries/local/base.py similarity index 71% rename from musify/local/base.py rename to musify/libraries/local/base.py index 232eee1b..8b9461cf 100644 --- a/musify/local/base.py +++ b/musify/libraries/local/base.py @@ -1,11 +1,10 @@ """ Core abstract classes for the :py:mod:`Local` module. """ - from abc import ABCMeta -from musify.shared.core.base import MusifyItem -from musify.shared.file import File +from musify.core.base import MusifyItem +from musify.file.base import File class LocalItem(File, MusifyItem, metaclass=ABCMeta): diff --git a/musify/local/collection.py b/musify/libraries/local/collection.py similarity index 93% rename from musify/local/collection.py rename to musify/libraries/local/collection.py index 717c986f..69f04823 100644 --- a/musify/local/collection.py +++ b/musify/libraries/local/collection.py @@ -1,7 +1,6 @@ """ Implements all collection types for a local library. """ - from __future__ import annotations import logging @@ -13,18 +12,17 @@ from os.path import splitext, join, basename, exists, isdir from typing import Any -from musify.local.base import LocalItem -from musify.local.exception import LocalCollectionError -from musify.local.track import LocalTrack, SyncResultTrack, load_track, TRACK_FILETYPES -from musify.local.track.field import LocalTrackField -from musify.shared.core.collection import MusifyCollection -from musify.shared.core.enum import Fields, TagField, TagFields -from musify.shared.core.object import Track, Library, Folder, Album, Artist, Genre -from musify.shared.logger import MusifyLogger -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.shared.types import UnitCollection -from musify.shared.types import UnitIterable -from musify.shared.utils import get_most_common_values, to_collection, align_string, get_max_width +from musify.core.enum import Fields, TagField, TagFields +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.core.object import Track, Library, Folder, Album, Artist, Genre +from musify.libraries.local.base import LocalItem +from musify.libraries.local.exception import LocalCollectionError +from musify.libraries.local.track import LocalTrack, SyncResultTrack, load_track, TRACK_FILETYPES +from musify.libraries.local.track.field import LocalTrackField +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.log.logger import MusifyLogger +from musify.types import UnitCollection, UnitIterable +from musify.utils import get_most_common_values, to_collection, align_string, get_max_width _max_str = "z" * 50 @@ -148,7 +146,7 @@ def merge_tracks(self, tracks: Collection[Track], tags: UnitIterable[TagField] = Merge this collection with another collection or list of items by performing an inner join on a given set of tags - :param tracks: List of items or ItemCollection to merge with + :param tracks: List of items or :py:class:`MusifyCollection` to merge with :param tags: List of tags to merge on. """ # noinspection PyTypeChecker diff --git a/musify/local/exception.py b/musify/libraries/local/exception.py similarity index 94% rename from musify/local/exception.py rename to musify/libraries/local/exception.py index 7ea7006b..09582d6d 100644 --- a/musify/local/exception.py +++ b/musify/libraries/local/exception.py @@ -1,8 +1,7 @@ """ Exceptions relating to local operations. """ - -from musify.shared.exception import MusifyError +from musify.exception import MusifyError class LocalError(MusifyError): diff --git a/musify/local/library/__init__.py b/musify/libraries/local/library/__init__.py similarity index 96% rename from musify/local/library/__init__.py rename to musify/libraries/local/library/__init__.py index 2d3e2ad9..e127120f 100644 --- a/musify/local/library/__init__.py +++ b/musify/libraries/local/library/__init__.py @@ -6,7 +6,6 @@ Specific library types should implement :py:class:`LocalLibrary`. """ - from .library import LocalLibrary from .musicbee import MusicBee diff --git a/musify/local/library/library.py b/musify/libraries/local/library/library.py similarity index 93% rename from musify/local/library/library.py rename to musify/libraries/local/library/library.py index 19a17ef3..7a72446e 100644 --- a/musify/local/library/library.py +++ b/musify/libraries/local/library/library.py @@ -8,21 +8,21 @@ from os.path import splitext, join, exists, basename, dirname from typing import Any -from musify.local.collection import LocalCollection, LocalFolder, LocalAlbum, LocalArtist, LocalGenres -from musify.local.playlist import PLAYLIST_CLASSES, LocalPlaylist, load_playlist -from musify.local.track import TRACK_CLASSES, LocalTrack, load_track -from musify.local.track.field import LocalTrackField +from musify.core.result import Result +from musify.exception import MusifyError +from musify.file.path_mapper import PathMapper, PathStemMapper +from musify.libraries.core.object import Playlist, Library +from musify.libraries.local.collection import LocalCollection, LocalFolder, LocalAlbum, LocalArtist, LocalGenres +from musify.libraries.local.playlist import PLAYLIST_CLASSES, LocalPlaylist, load_playlist +from musify.libraries.local.track import TRACK_CLASSES, LocalTrack, load_track +from musify.libraries.local.track.field import LocalTrackField +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.log import STAT from musify.processors.base import Filter from musify.processors.filter import FilterDefinedList from musify.processors.sort import ItemSorter -from musify.shared.core.misc import Result -from musify.shared.core.object import Playlist, Library -from musify.shared.exception import MusifyError -from musify.shared.file import PathMapper, PathStemMapper -from musify.shared.logger import STAT -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.shared.types import UnitCollection, UnitIterable -from musify.shared.utils import align_string, get_max_width, to_collection +from musify.types import UnitCollection, UnitIterable +from musify.utils import align_string, get_max_width, to_collection class LocalLibrary(LocalCollection[LocalTrack], Library[LocalTrack]): diff --git a/musify/local/library/musicbee.py b/musify/libraries/local/library/musicbee.py similarity index 95% rename from musify/local/library/musicbee.py rename to musify/libraries/local/library/musicbee.py index f13660ef..c64c2c22 100644 --- a/musify/local/library/musicbee.py +++ b/musify/libraries/local/library/musicbee.py @@ -2,7 +2,6 @@ An implementation of :py:class:`LocalLibrary` for the MusicBee library manager. Reads library/settings files from MusicBee to load and enrich playlist/track etc. data. """ - import hashlib import re import urllib.parse @@ -15,16 +14,17 @@ from lxml import etree from lxml.etree import iterparse -from musify.local.exception import MusicBeeIDError, XMLReaderError -from musify.local.library.library import LocalLibrary -from musify.local.playlist import LocalPlaylist -from musify.local.track import LocalTrack +from musify.file.base import File +from musify.file.exception import FileDoesNotExistError +from musify.file.path_mapper import PathMapper, PathStemMapper +from musify.libraries.local.exception import MusicBeeIDError, XMLReaderError +from musify.libraries.local.library.library import LocalLibrary +from musify.libraries.local.playlist import LocalPlaylist +from musify.libraries.local.track import LocalTrack +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler from musify.processors.base import Filter -from musify.shared.exception import FileDoesNotExistError -from musify.shared.file import File, PathMapper, PathStemMapper -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.shared.types import Number -from musify.shared.utils import to_collection +from musify.types import Number +from musify.utils import to_collection class MusicBee(LocalLibrary, File): diff --git a/musify/local/playlist/__init__.py b/musify/libraries/local/playlist/__init__.py similarity index 96% rename from musify/local/playlist/__init__.py rename to musify/libraries/local/playlist/__init__.py index 6e7ac286..f411909b 100644 --- a/musify/local/playlist/__init__.py +++ b/musify/libraries/local/playlist/__init__.py @@ -3,7 +3,6 @@ Specific audio file types should implement :py:class:`LocalPlaylist`. """ - from .base import LocalPlaylist from .m3u import M3U from .utils import PLAYLIST_CLASSES, PLAYLIST_FILETYPES, load_playlist diff --git a/musify/local/playlist/base.py b/musify/libraries/local/playlist/base.py similarity index 91% rename from musify/local/playlist/base.py rename to musify/libraries/local/playlist/base.py index 38725f42..75c9d855 100644 --- a/musify/local/playlist/base.py +++ b/musify/libraries/local/playlist/base.py @@ -1,21 +1,21 @@ """ Base implementation for the functionality of a local playlist. """ - from abc import ABCMeta, abstractmethod from collections.abc import Collection from datetime import datetime from os.path import dirname, join, getmtime, getctime, exists -from musify.local.collection import LocalCollection -from musify.local.track import LocalTrack +from musify.core.result import Result +from musify.file.base import File +from musify.file.path_mapper import PathMapper +from musify.libraries.core.object import Playlist +from musify.libraries.local.collection import LocalCollection +from musify.libraries.local.track import LocalTrack +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler from musify.processors.base import Filter from musify.processors.limit import ItemLimiter from musify.processors.sort import ItemSorter -from musify.shared.core.misc import Result -from musify.shared.core.object import Playlist -from musify.shared.file import File, PathMapper -from musify.shared.remote.processors.wrangle import RemoteDataWrangler class LocalPlaylist[T: Filter[LocalTrack]](LocalCollection[LocalTrack], Playlist[LocalTrack], File, metaclass=ABCMeta): diff --git a/musify/local/playlist/m3u.py b/musify/libraries/local/playlist/m3u.py similarity index 92% rename from musify/local/playlist/m3u.py rename to musify/libraries/local/playlist/m3u.py index cc74ecec..7884bfcc 100644 --- a/musify/local/playlist/m3u.py +++ b/musify/libraries/local/playlist/m3u.py @@ -1,18 +1,18 @@ """ The M3U implementation of a :py:class:`LocalPlaylist`. """ - import os from collections.abc import Collection from dataclasses import dataclass from os.path import exists, dirname -from musify.local.playlist.base import LocalPlaylist -from musify.local.track import LocalTrack, load_track +from musify.core.result import Result +from musify.file.base import File +from musify.file.path_mapper import PathMapper +from musify.libraries.local.playlist.base import LocalPlaylist +from musify.libraries.local.track import LocalTrack, load_track +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler from musify.processors.filter import FilterDefinedList -from musify.shared.core.misc import Result -from musify.shared.file import PathMapper, File -from musify.shared.remote.processors.wrangle import RemoteDataWrangler @dataclass(frozen=True) diff --git a/musify/local/playlist/utils.py b/musify/libraries/local/playlist/utils.py similarity index 84% rename from musify/local/playlist/utils.py rename to musify/libraries/local/playlist/utils.py index 62b52ba8..b0008969 100644 --- a/musify/local/playlist/utils.py +++ b/musify/libraries/local/playlist/utils.py @@ -4,17 +4,16 @@ Generally, this will contain global variables representing all supported playlist file types and a utility function for loading the appropriate :py:class:`LocalPlaylist` type for a path based on its extension. """ - from collections.abc import Collection from os.path import splitext -from musify.local.playlist.base import LocalPlaylist -from musify.local.playlist.m3u import M3U -from musify.local.playlist.xautopf import XAutoPF -from musify.local.track import LocalTrack -from musify.shared.exception import InvalidFileType -from musify.shared.file import PathMapper -from musify.shared.remote.processors.wrangle import RemoteDataWrangler +from musify.file.exception import InvalidFileType +from musify.file.path_mapper import PathMapper +from musify.libraries.local.playlist.base import LocalPlaylist +from musify.libraries.local.playlist.m3u import M3U +from musify.libraries.local.playlist.xautopf import XAutoPF +from musify.libraries.local.track import LocalTrack +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler PLAYLIST_CLASSES = frozenset({M3U, XAutoPF}) PLAYLIST_FILETYPES = frozenset(filetype for c in PLAYLIST_CLASSES for filetype in c.valid_extensions) diff --git a/musify/local/playlist/xautopf.py b/musify/libraries/local/playlist/xautopf.py similarity index 95% rename from musify/local/playlist/xautopf.py rename to musify/libraries/local/playlist/xautopf.py index bd8da4b0..7b3005dc 100644 --- a/musify/local/playlist/xautopf.py +++ b/musify/libraries/local/playlist/xautopf.py @@ -1,7 +1,6 @@ """ The XAutoPF implementation of a :py:class:`LocalPlaylist`. """ - from collections.abc import Collection from copy import deepcopy from dataclasses import dataclass @@ -10,15 +9,15 @@ import xmltodict -from musify.local.playlist.base import LocalPlaylist -from musify.local.track import LocalTrack +from musify.core.enum import Fields +from musify.core.result import Result +from musify.file.path_mapper import PathMapper +from musify.libraries.local.playlist.base import LocalPlaylist +from musify.libraries.local.track import LocalTrack from musify.processors.filter import FilterDefinedList, FilterComparers from musify.processors.filter_matcher import FilterMatcher from musify.processors.limit import ItemLimiter from musify.processors.sort import ItemSorter -from musify.shared.core.enum import Fields -from musify.shared.core.misc import Result -from musify.shared.file import PathMapper @dataclass(frozen=True) diff --git a/musify/local/track/__init__.py b/musify/libraries/local/track/__init__.py similarity index 96% rename from musify/local/track/__init__.py rename to musify/libraries/local/track/__init__.py index df04bfdd..62d0d4e0 100644 --- a/musify/local/track/__init__.py +++ b/musify/libraries/local/track/__init__.py @@ -3,7 +3,6 @@ Specific audio file types should implement :py:class:`LocalTrack`. """ - from .flac import FLAC from .m4a import M4A from .mp3 import MP3 diff --git a/musify/local/track/field.py b/musify/libraries/local/track/field.py similarity index 92% rename from musify/local/track/field.py rename to musify/libraries/local/track/field.py index 9e53b215..c76f52d4 100644 --- a/musify/local/track/field.py +++ b/musify/libraries/local/track/field.py @@ -1,9 +1,8 @@ """ The core Field enum for a :py:class:`LocalTrack` representing all possible tags/metadata/properties. """ - -from musify.shared.core.enum import TagFields -from musify.shared.field import TrackFieldMixin +from musify.core.enum import TagFields +from musify.field import TrackFieldMixin class LocalTrackField(TrackFieldMixin): diff --git a/musify/local/track/flac.py b/musify/libraries/local/track/flac.py similarity index 91% rename from musify/local/track/flac.py rename to musify/libraries/local/track/flac.py index a8c4d6ab..ec722a07 100644 --- a/musify/local/track/flac.py +++ b/musify/libraries/local/track/flac.py @@ -1,7 +1,6 @@ """ The FLAC implementation of a :py:class:`LocalTrack`. """ - from io import BytesIO from typing import Any @@ -10,12 +9,12 @@ import mutagen.id3 from PIL import Image -from musify.local.track.field import LocalTrackField -from musify.local.track.tags.reader import TagReader -from musify.local.track.tags.writer import TagWriter -from musify.local.track.track import LocalTrack -from musify.shared.core.enum import TagMap -from musify.shared.image import open_image, get_image_bytes +from musify.core.enum import TagMap +from musify.file.image import open_image, get_image_bytes +from musify.libraries.local.track.field import LocalTrackField +from musify.libraries.local.track.tags.reader import TagReader +from musify.libraries.local.track.tags.writer import TagWriter +from musify.libraries.local.track.track import LocalTrack class FLACTagReader(TagReader[mutagen.flac.FLAC]): diff --git a/musify/local/track/m4a.py b/musify/libraries/local/track/m4a.py similarity index 95% rename from musify/local/track/m4a.py rename to musify/libraries/local/track/m4a.py index 45376d92..cca16282 100644 --- a/musify/local/track/m4a.py +++ b/musify/libraries/local/track/m4a.py @@ -1,7 +1,6 @@ """ The M4A implementation of a :py:class:`LocalTrack`. """ - from collections.abc import Iterable from io import BytesIO from typing import Any @@ -10,12 +9,12 @@ import mutagen.mp4 from PIL import Image -from musify.local.track.tags.reader import TagReader -from musify.local.track.tags.writer import TagWriter -from musify.local.track.track import LocalTrack -from musify.shared.core.enum import TagMap -from musify.shared.image import open_image, get_image_bytes -from musify.shared.utils import to_collection +from musify.core.enum import TagMap +from musify.file.image import open_image, get_image_bytes +from musify.libraries.local.track.tags.reader import TagReader +from musify.libraries.local.track.tags.writer import TagWriter +from musify.libraries.local.track.track import LocalTrack +from musify.utils import to_collection class M4ATagReader(TagReader[mutagen.mp4.MP4]): diff --git a/musify/local/track/mp3.py b/musify/libraries/local/track/mp3.py similarity index 95% rename from musify/local/track/mp3.py rename to musify/libraries/local/track/mp3.py index 2284dd6d..58031c4a 100644 --- a/musify/local/track/mp3.py +++ b/musify/libraries/local/track/mp3.py @@ -1,7 +1,6 @@ """ The MP3 implementation of a :py:class:`LocalTrack`. """ - from collections.abc import Iterable from io import BytesIO from typing import Any @@ -11,12 +10,12 @@ import mutagen.mp3 from PIL import Image -from musify.local.track.field import LocalTrackField -from musify.local.track.tags.reader import TagReader -from musify.local.track.tags.writer import TagWriter -from musify.local.track.track import LocalTrack -from musify.shared.core.enum import TagMap -from musify.shared.image import open_image, get_image_bytes +from musify.core.enum import TagMap +from musify.file.image import open_image, get_image_bytes +from musify.libraries.local.track.field import LocalTrackField +from musify.libraries.local.track.tags.reader import TagReader +from musify.libraries.local.track.tags.writer import TagWriter +from musify.libraries.local.track.track import LocalTrack class MP3TagReader(TagReader[mutagen.mp3.MP3]): diff --git a/musify/local/track/tags/__init__.py b/musify/libraries/local/track/tags/__init__.py similarity index 100% rename from musify/local/track/tags/__init__.py rename to musify/libraries/local/track/tags/__init__.py diff --git a/musify/local/track/tags/base.py b/musify/libraries/local/track/tags/base.py similarity index 89% rename from musify/local/track/tags/base.py rename to musify/libraries/local/track/tags/base.py index 872f85bc..1786f6ff 100644 --- a/musify/local/track/tags/base.py +++ b/musify/libraries/local/track/tags/base.py @@ -1,14 +1,13 @@ """ The base processor definition for reading/manipulating tag data in an audio file. """ - from abc import ABC import mutagen -from musify.local.track.field import LocalTrackField -from musify.shared.core.enum import TagMap -from musify.shared.remote.processors.wrangle import RemoteDataWrangler +from musify.core.enum import TagMap +from musify.libraries.local.track.field import LocalTrackField +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler class TagProcessor[T: mutagen.FileType](ABC): diff --git a/musify/local/track/tags/reader.py b/musify/libraries/local/track/tags/reader.py similarity index 95% rename from musify/local/track/tags/reader.py rename to musify/libraries/local/track/tags/reader.py index 23820b96..125ca6de 100644 --- a/musify/local/track/tags/reader.py +++ b/musify/libraries/local/track/tags/reader.py @@ -1,7 +1,6 @@ """ Implements all functionality pertaining to reading metadata/tags/properties for a :py:class:`LocalTrack`. """ - import re from abc import ABCMeta, abstractmethod from collections.abc import Iterable @@ -10,9 +9,9 @@ import mutagen from PIL import Image -from musify.local.track.tags.base import TagProcessor -from musify.shared.remote.enum import RemoteIDType -from musify.shared.utils import to_collection +from musify.libraries.local.track.tags.base import TagProcessor +from musify.libraries.remote.core.enum import RemoteIDType +from musify.utils import to_collection class TagReader[T: mutagen.FileType](TagProcessor, metaclass=ABCMeta): diff --git a/musify/local/track/tags/writer.py b/musify/libraries/local/track/tags/writer.py similarity index 97% rename from musify/local/track/tags/writer.py rename to musify/libraries/local/track/tags/writer.py index 27d8224f..2d6c98a5 100644 --- a/musify/local/track/tags/writer.py +++ b/musify/libraries/local/track/tags/writer.py @@ -1,7 +1,6 @@ """ Implements all functionality pertaining to writing and deleting metadata/tags/properties for a :py:class:`LocalTrack`. """ - from abc import ABCMeta, abstractmethod from collections.abc import Mapping, Collection from dataclasses import dataclass @@ -9,12 +8,12 @@ import mutagen -from musify.local.track.field import LocalTrackField as Tags -from musify.local.track.tags.base import TagProcessor -from musify.shared.core.misc import Result -from musify.shared.core.object import Track -from musify.shared.types import UnitIterable -from musify.shared.utils import to_collection +from musify.core.result import Result +from musify.libraries.core.object import Track +from musify.libraries.local.track.field import LocalTrackField as Tags +from musify.libraries.local.track.tags.base import TagProcessor +from musify.types import UnitIterable +from musify.utils import to_collection @dataclass(frozen=True) diff --git a/musify/local/track/track.py b/musify/libraries/local/track/track.py similarity index 91% rename from musify/local/track/track.py rename to musify/libraries/local/track/track.py index 4a431573..1bc9497c 100644 --- a/musify/local/track/track.py +++ b/musify/libraries/local/track/track.py @@ -5,25 +5,24 @@ import os from abc import ABCMeta, abstractmethod from copy import deepcopy -from glob import glob from os.path import join, exists, dirname from typing import Any, Self import mutagen -from musify.local.base import LocalItem -from musify.local.track.field import LocalTrackField as Tags -from musify.local.track.tags.reader import TagReader -from musify.local.track.tags.writer import TagWriter, SyncResultTrack -from musify.shared.core.base import MusifyItem -from musify.shared.core.enum import TagMap -from musify.shared.core.object import Track -from musify.shared.exception import FileDoesNotExistError -from musify.shared.exception import MusifyKeyError, MusifyAttributeError, MusifyTypeError, MusifyValueError -from musify.shared.field import TrackField -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.shared.types import UnitIterable -from musify.shared.utils import to_collection +from musify.core.base import MusifyItem +from musify.core.enum import TagMap +from musify.exception import MusifyKeyError, MusifyAttributeError, MusifyTypeError, MusifyValueError +from musify.field import TrackField +from musify.file.exception import FileDoesNotExistError +from musify.libraries.core.object import Track +from musify.libraries.local.base import LocalItem +from musify.libraries.local.track.field import LocalTrackField as Tags +from musify.libraries.local.track.tags.reader import TagReader +from musify.libraries.local.track.tags.writer import TagWriter, SyncResultTrack +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.types import UnitIterable +from musify.utils import to_collection class LocalTrack[T: mutagen.FileType, U: TagReader, V: TagWriter](LocalItem, Track, metaclass=ABCMeta): @@ -343,20 +342,6 @@ def play_count(self) -> int | None: def play_count(self, value: int | None): self._play_count = value - @classmethod - def get_filepaths(cls, library_folder: str) -> set[str]: - """Get all files in a given library that match this Track object's valid filetypes.""" - paths = set() - - for ext in cls.valid_extensions: - # first glob doesn't get filenames that start with a period - paths |= set(glob(join(library_folder, "**", f"*{ext}"), recursive=True)) - # second glob only picks up filenames that start with a period - paths |= set(glob(join(library_folder, "*", "**", f".*{ext}"), recursive=True)) - - # do not return paths in the recycle bin in Windows-based folders - return {path for path in paths if "$RECYCLE.BIN" not in path} - @staticmethod @abstractmethod def _create_reader(*args, **kwargs) -> U: diff --git a/musify/local/track/utils.py b/musify/libraries/local/track/utils.py similarity index 78% rename from musify/local/track/utils.py rename to musify/libraries/local/track/utils.py index f6b62d80..0258b27c 100644 --- a/musify/local/track/utils.py +++ b/musify/libraries/local/track/utils.py @@ -4,16 +4,15 @@ Generally, this will contain global variables representing all supported audio file types and a utility function for loading the appropriate :py:class:`LocalTrack` type for a path based on its extension. """ - from os.path import splitext -from musify.local.track import LocalTrack -from musify.local.track.flac import FLAC -from musify.local.track.m4a import M4A -from musify.local.track.mp3 import MP3 -from musify.local.track.wma import WMA -from musify.shared.exception import InvalidFileType -from musify.shared.remote.processors.wrangle import RemoteDataWrangler +from musify.file.exception import InvalidFileType +from musify.libraries.local.track import LocalTrack +from musify.libraries.local.track.flac import FLAC +from musify.libraries.local.track.m4a import M4A +from musify.libraries.local.track.mp3 import MP3 +from musify.libraries.local.track.wma import WMA +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler TRACK_CLASSES = frozenset({FLAC, MP3, M4A, WMA}) TRACK_FILETYPES = frozenset(filetype for c in TRACK_CLASSES for filetype in c.valid_extensions) diff --git a/musify/local/track/wma.py b/musify/libraries/local/track/wma.py similarity index 94% rename from musify/local/track/wma.py rename to musify/libraries/local/track/wma.py index 753ad56f..689c03c6 100644 --- a/musify/local/track/wma.py +++ b/musify/libraries/local/track/wma.py @@ -1,7 +1,6 @@ """ The WMA implementation of a :py:class:`LocalTrack`. """ - import struct from collections.abc import Collection, Iterable from io import BytesIO @@ -12,11 +11,11 @@ import mutagen.id3 from PIL import Image, UnidentifiedImageError -from musify.local.track.tags.reader import TagReader -from musify.local.track.tags.writer import TagWriter -from musify.local.track.track import LocalTrack -from musify.shared.core.enum import TagMap -from musify.shared.image import open_image, get_image_bytes +from musify.core.enum import TagMap +from musify.file.image import open_image, get_image_bytes +from musify.libraries.local.track.tags.reader import TagReader +from musify.libraries.local.track.tags.writer import TagWriter +from musify.libraries.local.track.track import LocalTrack class WMATagReader(TagReader[mutagen.asf.ASF]): diff --git a/musify/libraries/remote/__init__.py b/musify/libraries/remote/__init__.py new file mode 100644 index 00000000..a6804fd4 --- /dev/null +++ b/musify/libraries/remote/__init__.py @@ -0,0 +1,3 @@ +""" +All remote classes and operations with remote objects, files, tracks, albums, playlists, libraries etc. +""" diff --git a/musify/shared/remote/__init__.py b/musify/libraries/remote/core/__init__.py similarity index 96% rename from musify/shared/remote/__init__.py rename to musify/libraries/remote/core/__init__.py index e86ae0b1..2db1b391 100644 --- a/musify/shared/remote/__init__.py +++ b/musify/libraries/remote/core/__init__.py @@ -7,5 +7,4 @@ Also defines abstract classes to represent objects derived from the types of responses that can be returned by the API. """ - from .response import RemoteResponse diff --git a/musify/shared/remote/api.py b/musify/libraries/remote/core/api.py similarity index 95% rename from musify/shared/remote/api.py rename to musify/libraries/remote/core/api.py index 2e806ffc..aea7256c 100644 --- a/musify/shared/remote/api.py +++ b/musify/libraries/remote/core/api.py @@ -3,20 +3,19 @@ All methods that interact with the API should return raw, unprocessed responses. """ - import logging from abc import ABC, abstractmethod from collections.abc import Collection, MutableMapping, Mapping, Sequence from typing import Any, Self -from musify.shared.api.request import RequestHandler -from musify.shared.logger import MusifyLogger -from musify.shared.remote import RemoteResponse -from musify.shared.remote.enum import RemoteIDType, RemoteObjectType -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.shared.remote.types import APIInputValue -from musify.shared.types import UnitSequence, JSON, UnitList -from musify.shared.utils import align_string, to_collection +from musify.api.request import RequestHandler +from musify.libraries.remote.core import RemoteResponse +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.types import APIInputValue +from musify.log.logger import MusifyLogger +from musify.types import UnitSequence, JSON, UnitList +from musify.utils import align_string, to_collection class RemoteAPI(ABC): diff --git a/musify/shared/remote/base.py b/musify/libraries/remote/core/base.py similarity index 92% rename from musify/shared/remote/base.py rename to musify/libraries/remote/core/base.py index 0294b0c1..9cc4f682 100644 --- a/musify/shared/remote/base.py +++ b/musify/libraries/remote/core/base.py @@ -3,15 +3,14 @@ These define the foundations of any remote object or item. """ - from abc import ABCMeta, abstractmethod from collections.abc import Mapping from typing import Any, Self -from musify.shared.api.exception import APIError -from musify.shared.core.base import MusifyItem -from musify.shared.remote import RemoteResponse -from musify.shared.remote.api import RemoteAPI +from musify.api.exception import APIError +from musify.core.base import MusifyItem +from musify.libraries.remote.core import RemoteResponse +from musify.libraries.remote.core.api import RemoteAPI class RemoteObject[T: (RemoteAPI | None)](RemoteResponse, metaclass=ABCMeta): diff --git a/musify/shared/remote/enum.py b/musify/libraries/remote/core/enum.py similarity index 88% rename from musify/shared/remote/enum.py rename to musify/libraries/remote/core/enum.py index 2d1bf7ee..e441e1d3 100644 --- a/musify/shared/remote/enum.py +++ b/musify/libraries/remote/core/enum.py @@ -3,8 +3,7 @@ Represents ID and item types. """ - -from musify.shared.core.enum import MusifyEnum +from musify.core.enum import MusifyEnum class RemoteIDType(MusifyEnum): diff --git a/musify/shared/remote/exception.py b/musify/libraries/remote/core/exception.py similarity index 89% rename from musify/shared/remote/exception.py rename to musify/libraries/remote/core/exception.py index cfb4b01c..e2e83448 100644 --- a/musify/shared/remote/exception.py +++ b/musify/libraries/remote/core/exception.py @@ -1,11 +1,10 @@ """ Exceptions relating to remote operations. """ - from typing import Any -from musify.shared.exception import MusifyError -from musify.shared.remote.enum import RemoteIDType, RemoteObjectType +from musify.exception import MusifyError +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType class RemoteError(MusifyError): diff --git a/musify/shared/remote/factory.py b/musify/libraries/remote/core/factory.py similarity index 77% rename from musify/shared/remote/factory.py rename to musify/libraries/remote/core/factory.py index 4501f4f2..68543998 100644 --- a/musify/shared/remote/factory.py +++ b/musify/libraries/remote/core/factory.py @@ -7,10 +7,10 @@ from dataclasses import dataclass from functools import partial -from musify.shared.remote.api import RemoteAPI -from musify.shared.remote.base import RemoteObject -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.object import RemoteTrack, RemoteAlbum, RemotePlaylist, RemoteArtist +from musify.libraries.remote.core.api import RemoteAPI +from musify.libraries.remote.core.base import RemoteObject +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.object import RemoteTrack, RemoteAlbum, RemotePlaylist, RemoteArtist @dataclass @@ -35,7 +35,7 @@ def __getattribute__(self, __name: str): if inspect.isclass(attribute) and issubclass(attribute, RemoteObject) and self.api is not None: executable = partial(attribute, api=self.api) - # need to assign the classmethods back to the partial object to ensure near seamless user use + # need to assign the class methods back to the partial object to ensure near seamless user use for key in dir(attribute): value = getattr(attribute, key) if inspect.ismethod(value) and not key.startswith("_"): diff --git a/musify/shared/remote/library.py b/musify/libraries/remote/core/library.py similarity index 97% rename from musify/shared/remote/library.py rename to musify/libraries/remote/core/library.py index 6b118976..11334393 100644 --- a/musify/shared/remote/library.py +++ b/musify/libraries/remote/core/library.py @@ -1,22 +1,21 @@ """ Functionality relating to a generic remote library. """ - from abc import ABCMeta, abstractmethod from collections.abc import Collection, Mapping, Iterable from typing import Any, Literal +from musify.core.base import MusifyItem +from musify.libraries.core.object import Track, Library, Playlist +from musify.libraries.remote.core.api import RemoteAPI +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.factory import RemoteObjectFactory +from musify.libraries.remote.core.object import RemoteCollection, SyncResultRemotePlaylist +from musify.libraries.remote.core.object import RemoteTrack, RemotePlaylist, RemoteArtist, RemoteAlbum +from musify.log import STAT from musify.processors.base import Filter from musify.processors.filter import FilterDefinedList -from musify.shared.core.base import MusifyItem -from musify.shared.core.object import Track, Library, Playlist -from musify.shared.logger import STAT -from musify.shared.remote.api import RemoteAPI -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.factory import RemoteObjectFactory -from musify.shared.remote.object import RemoteCollection, SyncResultRemotePlaylist -from musify.shared.remote.object import RemoteTrack, RemotePlaylist, RemoteArtist, RemoteAlbum -from musify.shared.utils import align_string, get_max_width +from musify.utils import align_string, get_max_width class RemoteLibrary[ diff --git a/musify/shared/remote/object.py b/musify/libraries/remote/core/object.py similarity index 95% rename from musify/shared/remote/object.py rename to musify/libraries/remote/core/object.py index a23ce4ef..44969827 100644 --- a/musify/shared/remote/object.py +++ b/musify/libraries/remote/core/object.py @@ -1,9 +1,8 @@ """ Functionality relating to generic remote objects. -Implements core :py:class:`Item` and :py:class:`ItemCollection` classes for remote object types. +Implements core :py:class:`MusifyItem` and :py:class:`MusifyCollection` classes for remote object types. """ - from __future__ import annotations from abc import ABCMeta, abstractmethod @@ -12,16 +11,16 @@ from datetime import datetime from typing import Self, Literal, Any -from musify.shared.api.exception import APIError -from musify.shared.core.base import MusifyItem -from musify.shared.core.collection import MusifyCollection -from musify.shared.core.misc import Result -from musify.shared.core.object import Track, Album, Playlist, Artist -from musify.shared.remote.api import RemoteAPI -from musify.shared.remote.base import RemoteObject, RemoteItem -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.exception import RemoteError -from musify.shared.utils import get_most_common_values +from musify.api.exception import APIError +from musify.core.base import MusifyItem +from musify.core.result import Result +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.core.object import Track, Album, Playlist, Artist +from musify.libraries.remote.core.api import RemoteAPI +from musify.libraries.remote.core.base import RemoteObject, RemoteItem +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.exception import RemoteError +from musify.utils import get_most_common_values class RemoteTrack(RemoteItem, Track, metaclass=ABCMeta): diff --git a/musify/shared/remote/processors/__init__.py b/musify/libraries/remote/core/processors/__init__.py similarity index 100% rename from musify/shared/remote/processors/__init__.py rename to musify/libraries/remote/core/processors/__init__.py diff --git a/musify/shared/remote/processors/check.py b/musify/libraries/remote/core/processors/check.py similarity index 94% rename from musify/shared/remote/processors/check.py rename to musify/libraries/remote/core/processors/check.py index 3816e9af..fd7266ef 100644 --- a/musify/shared/remote/processors/check.py +++ b/musify/libraries/remote/core/processors/check.py @@ -4,25 +4,24 @@ Provides the user the ability to modify associated IDs using a Remote player as an interface for reviewing matches through temporary playlist creation. """ - import traceback from collections import Counter from collections.abc import Sequence, Collection from dataclasses import dataclass, field from musify import PROGRAM_NAME +from musify.core.base import MusifyItem +from musify.core.enum import Fields +from musify.core.result import Result +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.core.object import Track +from musify.libraries.remote.core.enum import RemoteObjectType, RemoteIDType +from musify.libraries.remote.core.factory import RemoteObjectFactory +from musify.libraries.remote.core.processors.search import RemoteItemSearcher +from musify.log import REPORT from musify.processors.base import InputProcessor from musify.processors.match import ItemMatcher -from musify.shared.core.base import MusifyItem -from musify.shared.core.collection import MusifyCollection -from musify.shared.core.enum import Fields -from musify.shared.core.misc import Result -from musify.shared.core.object import Track -from musify.shared.logger import REPORT -from musify.shared.remote.enum import RemoteObjectType, RemoteIDType -from musify.shared.remote.factory import RemoteObjectFactory -from musify.shared.remote.processors.search import RemoteItemSearcher -from musify.shared.utils import get_max_width, align_string +from musify.utils import get_max_width, align_string ALLOW_KARAOKE_DEFAULT = RemoteItemSearcher.settings_items.allow_karaoke @@ -142,9 +141,8 @@ def _delete_playlists(self) -> None: self._playlist_name_urls.clear() self._playlist_name_collection.clear() - # noinspection PyMethodOverriding - def __call__(self, collections: Collection[MusifyCollection]) -> ItemCheckResult | None: - return self.check(collections=collections) + def __call__(self, *args, **kwargs) -> ItemCheckResult | None: + return self.check(*args, **kwargs) def check(self, collections: Collection[MusifyCollection]) -> ItemCheckResult | None: """ diff --git a/musify/shared/remote/processors/search.py b/musify/libraries/remote/core/processors/search.py similarity index 92% rename from musify/shared/remote/processors/search.py rename to musify/libraries/remote/core/processors/search.py index 6711f45f..8c7f470b 100644 --- a/musify/shared/remote/processors/search.py +++ b/musify/libraries/remote/core/processors/search.py @@ -4,21 +4,20 @@ Searches for matches on remote APIs, matches the item to the best matching result from the query, and assigns the ID of the matched object back to the item. """ - from collections.abc import Mapping, Sequence, Iterable, Collection from dataclasses import dataclass, field from typing import Any +from musify.core.base import MusifyObject, MusifyItem +from musify.core.enum import TagField, TagFields as Tag +from musify.core.result import Result +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.core.object import Track +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.factory import RemoteObjectFactory +from musify.log import REPORT from musify.processors.match import ItemMatcher -from musify.shared.core.base import MusifyObject, MusifyItem -from musify.shared.core.collection import MusifyCollection -from musify.shared.core.enum import TagField, TagFields as Tag -from musify.shared.core.misc import Result -from musify.shared.core.object import Track -from musify.shared.logger import REPORT -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.factory import RemoteObjectFactory -from musify.shared.utils import align_string, get_max_width +from musify.utils import align_string, get_max_width @dataclass(frozen=True) @@ -169,9 +168,8 @@ def _log_results(self, results: Mapping[str, ItemSearchResult]) -> None: ) self.logger.print(REPORT) - # noinspection PyMethodOverriding - def __call__(self, collections: Collection[MusifyCollection]) -> dict[str, ItemSearchResult]: - return self.search(collections=collections) + def __call__(self, *args, **kwargs) -> dict[str, ItemSearchResult]: + return self.search(*args, **kwargs) def search(self, collections: Collection[MusifyCollection]) -> dict[str, ItemSearchResult]: """ diff --git a/musify/shared/remote/processors/wrangle.py b/musify/libraries/remote/core/processors/wrangle.py similarity index 95% rename from musify/shared/remote/processors/wrangle.py rename to musify/libraries/remote/core/processors/wrangle.py index 1d44cbc4..5a365816 100644 --- a/musify/shared/remote/processors/wrangle.py +++ b/musify/libraries/remote/core/processors/wrangle.py @@ -1,15 +1,14 @@ """ Convert and validate remote ID and item types according to specific remote implementations. """ - from abc import ABC, abstractmethod from collections.abc import Mapping from typing import Any -from musify.shared.remote import RemoteResponse -from musify.shared.remote.enum import RemoteIDType, RemoteObjectType -from musify.shared.remote.exception import RemoteObjectTypeError -from musify.shared.remote.types import APIInputValue +from musify.libraries.remote.core import RemoteResponse +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType +from musify.libraries.remote.core.exception import RemoteObjectTypeError +from musify.libraries.remote.core.types import APIInputValue class RemoteDataWrangler(ABC): diff --git a/musify/shared/remote/response.py b/musify/libraries/remote/core/response.py similarity index 88% rename from musify/shared/remote/response.py rename to musify/libraries/remote/core/response.py index 999130b8..afc0652c 100644 --- a/musify/shared/remote/response.py +++ b/musify/libraries/remote/core/response.py @@ -2,12 +2,11 @@ Just the core abstract class for the :py:mod:`Remote` module. Placed here separately to avoid circular import logic issues. """ - from abc import abstractmethod, ABCMeta from typing import Any -from musify.shared.core.base import MusifyObject -from musify.shared.remote.enum import RemoteObjectType +from musify.core.base import MusifyObject +from musify.libraries.remote.core.enum import RemoteObjectType class RemoteResponse(MusifyObject, metaclass=ABCMeta): diff --git a/musify/shared/remote/types.py b/musify/libraries/remote/core/types.py similarity index 72% rename from musify/shared/remote/types.py rename to musify/libraries/remote/core/types.py index fd38bb14..27db988d 100644 --- a/musify/shared/remote/types.py +++ b/musify/libraries/remote/core/types.py @@ -1,12 +1,11 @@ """ All type hints to use throughout the module. """ - from collections.abc import MutableMapping from typing import Any, TypeVar -from musify.shared.remote import RemoteResponse -from musify.shared.types import UnitMutableSequence, UnitSequence +from musify.libraries.remote.core import RemoteResponse +from musify.types import UnitMutableSequence, UnitSequence UT = TypeVar('UT') APIInputValue = ( diff --git a/musify/spotify/__init__.py b/musify/libraries/remote/spotify/__init__.py similarity index 100% rename from musify/spotify/__init__.py rename to musify/libraries/remote/spotify/__init__.py diff --git a/musify/spotify/api/__init__.py b/musify/libraries/remote/spotify/api/__init__.py similarity index 94% rename from musify/spotify/api/__init__.py rename to musify/libraries/remote/spotify/api/__init__.py index ece51983..161dec35 100644 --- a/musify/spotify/api/__init__.py +++ b/musify/libraries/remote/spotify/api/__init__.py @@ -1,5 +1,4 @@ """ Implements all required :py:class:`RemoteAPI` functionality for Spotify. """ - from .api import SpotifyAPI diff --git a/musify/spotify/api/api.py b/musify/libraries/remote/spotify/api/api.py similarity index 87% rename from musify/spotify/api/api.py rename to musify/libraries/remote/spotify/api/api.py index ea300941..e35c5fdc 100644 --- a/musify/spotify/api/api.py +++ b/musify/libraries/remote/spotify/api/api.py @@ -3,18 +3,17 @@ Also includes the default arguments to be used when requesting authorisation from the Spotify API. """ - import base64 from collections.abc import Iterable from copy import deepcopy from musify import PROGRAM_NAME -from musify.shared.api.exception import APIError -from musify.shared.utils import safe_format_map -from musify.spotify.api.item import SpotifyAPIItems -from musify.spotify.api.misc import SpotifyAPIMisc -from musify.spotify.api.playlist import SpotifyAPIPlaylists -from musify.spotify.processors import SpotifyDataWrangler +from musify.api.exception import APIError +from musify.libraries.remote.spotify.api.item import SpotifyAPIItems +from musify.libraries.remote.spotify.api.misc import SpotifyAPIMisc +from musify.libraries.remote.spotify.api.playlist import SpotifyAPIPlaylists +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.utils import safe_format_map URL_AUTH = "https://accounts.spotify.com" diff --git a/musify/spotify/api/base.py b/musify/libraries/remote/spotify/api/base.py similarity index 92% rename from musify/spotify/api/base.py rename to musify/libraries/remote/spotify/api/base.py index 11679ee8..75ebaebb 100644 --- a/musify/spotify/api/base.py +++ b/musify/libraries/remote/spotify/api/base.py @@ -1,12 +1,11 @@ """ Base functionality to be shared by all classes that implement :py:class:`RemoteAPI` functionality for Spotify. """ - from abc import ABCMeta from typing import Any from urllib.parse import parse_qs, urlparse, urlencode, quote, urlunparse -from musify.shared.remote.api import RemoteAPI +from musify.libraries.remote.core.api import RemoteAPI class SpotifyAPIBase(RemoteAPI, metaclass=ABCMeta): diff --git a/musify/spotify/api/item.py b/musify/libraries/remote/spotify/api/item.py similarity index 96% rename from musify/spotify/api/item.py rename to musify/libraries/remote/spotify/api/item.py index 4f072ff3..bdf15f0d 100644 --- a/musify/spotify/api/item.py +++ b/musify/libraries/remote/spotify/api/item.py @@ -1,7 +1,6 @@ """ Implements endpoints for getting items from the Spotify API. """ - import re from abc import ABCMeta from collections.abc import Collection, Mapping, MutableMapping @@ -9,13 +8,13 @@ from typing import Any from urllib.parse import parse_qs, urlparse -from musify.shared.api.exception import APIError -from musify.shared.remote import RemoteResponse -from musify.shared.remote.enum import RemoteObjectType, RemoteIDType -from musify.shared.remote.exception import RemoteObjectTypeError -from musify.shared.remote.types import APIInputValue -from musify.shared.utils import limit_value -from musify.spotify.api.base import SpotifyAPIBase +from musify.api.exception import APIError +from musify.libraries.remote.core import RemoteResponse +from musify.libraries.remote.core.enum import RemoteObjectType, RemoteIDType +from musify.libraries.remote.core.exception import RemoteObjectTypeError +from musify.libraries.remote.core.types import APIInputValue +from musify.libraries.remote.spotify.api.base import SpotifyAPIBase +from musify.utils import limit_value ARTIST_ALBUM_TYPES = {"album", "single", "compilation", "appears_on"} diff --git a/musify/spotify/api/misc.py b/musify/libraries/remote/spotify/api/misc.py similarity index 93% rename from musify/spotify/api/misc.py rename to musify/libraries/remote/spotify/api/misc.py index f4b187b6..577d9e38 100644 --- a/musify/spotify/api/misc.py +++ b/musify/libraries/remote/spotify/api/misc.py @@ -1,15 +1,14 @@ """ Implements all required non-items and non-playlist endpoints from the Spotify API. """ - from abc import ABCMeta from collections.abc import MutableMapping from typing import Any -from musify.shared.remote.enum import RemoteIDType, RemoteObjectType -from musify.shared.types import Number -from musify.shared.utils import limit_value -from musify.spotify.api.base import SpotifyAPIBase +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType +from musify.libraries.remote.spotify.api.base import SpotifyAPIBase +from musify.types import Number +from musify.utils import limit_value class SpotifyAPIMisc(SpotifyAPIBase, metaclass=ABCMeta): diff --git a/musify/spotify/api/playlist.py b/musify/libraries/remote/spotify/api/playlist.py similarity index 95% rename from musify/spotify/api/playlist.py rename to musify/libraries/remote/spotify/api/playlist.py index 2ceaa6f1..1d8ff011 100644 --- a/musify/spotify/api/playlist.py +++ b/musify/libraries/remote/spotify/api/playlist.py @@ -1,18 +1,17 @@ """ Implements endpoints for manipulating playlists with the Spotify API. """ - from abc import ABCMeta from collections.abc import Collection, Mapping from itertools import batched from typing import Any from musify import PROGRAM_NAME, PROGRAM_URL -from musify.shared.remote import RemoteResponse -from musify.shared.remote.enum import RemoteIDType, RemoteObjectType -from musify.shared.remote.exception import RemoteIDTypeError -from musify.shared.utils import limit_value -from musify.spotify.api.base import SpotifyAPIBase +from musify.libraries.remote.core import RemoteResponse +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType +from musify.libraries.remote.core.exception import RemoteIDTypeError +from musify.libraries.remote.spotify.api.base import SpotifyAPIBase +from musify.utils import limit_value class SpotifyAPIPlaylists(SpotifyAPIBase, metaclass=ABCMeta): diff --git a/musify/spotify/base.py b/musify/libraries/remote/spotify/base.py similarity index 84% rename from musify/spotify/base.py rename to musify/libraries/remote/spotify/base.py index f8dce479..fa1c2202 100644 --- a/musify/spotify/base.py +++ b/musify/libraries/remote/spotify/base.py @@ -3,14 +3,13 @@ These define the foundations of any Spotify object or item. """ - from abc import ABCMeta from typing import Any -from musify.shared.remote.base import RemoteObject, RemoteItem -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.exception import RemoteObjectTypeError, RemoteError -from musify.spotify.api import SpotifyAPI +from musify.libraries.remote.core.base import RemoteObject, RemoteItem +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.exception import RemoteObjectTypeError, RemoteError +from musify.libraries.remote.spotify.api import SpotifyAPI class SpotifyObject(RemoteObject[SpotifyAPI], metaclass=ABCMeta): diff --git a/musify/spotify/exception.py b/musify/libraries/remote/spotify/exception.py similarity index 91% rename from musify/spotify/exception.py rename to musify/libraries/remote/spotify/exception.py index 033e5131..7b2cad80 100644 --- a/musify/spotify/exception.py +++ b/musify/libraries/remote/spotify/exception.py @@ -1,8 +1,7 @@ """ Exceptions relating to Spotify operations. """ - -from musify.shared.remote.exception import RemoteError +from musify.libraries.remote.core.exception import RemoteError class SpotifyError(RemoteError): diff --git a/musify/spotify/factory.py b/musify/libraries/remote/spotify/factory.py similarity index 69% rename from musify/spotify/factory.py rename to musify/libraries/remote/spotify/factory.py index d3977e2f..bb387802 100644 --- a/musify/spotify/factory.py +++ b/musify/libraries/remote/spotify/factory.py @@ -5,8 +5,8 @@ """ from dataclasses import dataclass -from musify.shared.remote.factory import RemoteObjectFactory -from musify.spotify.object import SpotifyPlaylist, SpotifyTrack, SpotifyAlbum, SpotifyArtist +from musify.libraries.remote.core.factory import RemoteObjectFactory +from musify.libraries.remote.spotify.object import SpotifyPlaylist, SpotifyTrack, SpotifyAlbum, SpotifyArtist @dataclass diff --git a/musify/spotify/library.py b/musify/libraries/remote/spotify/library.py similarity index 93% rename from musify/spotify/library.py rename to musify/libraries/remote/spotify/library.py index cfaabefb..aad32d9d 100644 --- a/musify/spotify/library.py +++ b/musify/libraries/remote/spotify/library.py @@ -1,17 +1,16 @@ """ Implements a :py:class:`RemoteLibrary` for Spotify. """ - from collections.abc import Collection, Mapping, Iterable from typing import Any -from musify.shared.core.object import Playlist, Library -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.library import RemoteLibrary -from musify.shared.remote.object import RemoteTrack -from musify.spotify.api import SpotifyAPI -from musify.spotify.factory import SpotifyObjectFactory -from musify.spotify.object import SpotifyTrack, SpotifyAlbum, SpotifyArtist, SpotifyPlaylist +from musify.libraries.core.object import Playlist, Library +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.library import RemoteLibrary +from musify.libraries.remote.core.object import RemoteTrack +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.factory import SpotifyObjectFactory +from musify.libraries.remote.spotify.object import SpotifyTrack, SpotifyAlbum, SpotifyArtist, SpotifyPlaylist class SpotifyLibrary(RemoteLibrary[SpotifyAPI, SpotifyPlaylist, SpotifyTrack, SpotifyAlbum, SpotifyArtist]): diff --git a/musify/spotify/object.py b/musify/libraries/remote/spotify/object.py similarity index 98% rename from musify/spotify/object.py rename to musify/libraries/remote/spotify/object.py index 3983260b..77af9492 100644 --- a/musify/spotify/object.py +++ b/musify/libraries/remote/spotify/object.py @@ -1,7 +1,6 @@ """ Implements all :py:mod:`Remote` object types for Spotify. """ - from __future__ import annotations from abc import ABCMeta, abstractmethod @@ -10,15 +9,15 @@ from datetime import datetime from typing import Any, Self -from musify.shared.remote import RemoteResponse -from musify.shared.remote.enum import RemoteObjectType, RemoteIDType -from musify.shared.remote.object import RemoteCollectionLoader, RemoteTrack -from musify.shared.remote.object import RemotePlaylist, RemoteAlbum, RemoteArtist -from musify.shared.types import UnitCollection -from musify.shared.utils import to_collection -from musify.spotify.api import SpotifyAPI -from musify.spotify.base import SpotifyObject, SpotifyItem -from musify.spotify.exception import SpotifyCollectionError +from musify.libraries.remote.core import RemoteResponse +from musify.libraries.remote.core.enum import RemoteObjectType, RemoteIDType +from musify.libraries.remote.core.object import RemoteCollectionLoader, RemoteTrack +from musify.libraries.remote.core.object import RemotePlaylist, RemoteAlbum, RemoteArtist +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.base import SpotifyObject, SpotifyItem +from musify.libraries.remote.spotify.exception import SpotifyCollectionError +from musify.types import UnitCollection +from musify.utils import to_collection class SpotifyTrack(SpotifyItem, RemoteTrack): diff --git a/musify/spotify/processors.py b/musify/libraries/remote/spotify/processors.py similarity index 90% rename from musify/spotify/processors.py rename to musify/libraries/remote/spotify/processors.py index f7552853..ac9f631a 100644 --- a/musify/spotify/processors.py +++ b/musify/libraries/remote/spotify/processors.py @@ -1,19 +1,18 @@ """ Convert and validate Spotify ID and item types. """ - from collections.abc import Mapping from typing import Any from urllib.parse import urlparse -from musify.shared.core.collection import MusifyCollection -from musify.shared.exception import MusifyEnumError -from musify.shared.remote import RemoteResponse -from musify.shared.remote.api import APIInputValue -from musify.shared.remote.enum import RemoteIDType, RemoteObjectType -from musify.shared.remote.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.shared.utils import to_collection +from musify.exception import MusifyEnumError +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.remote.core import RemoteResponse +from musify.libraries.remote.core.api import APIInputValue +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType +from musify.libraries.remote.core.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.utils import to_collection class SpotifyDataWrangler(RemoteDataWrangler): diff --git a/musify/log/__init__.py b/musify/log/__init__.py new file mode 100644 index 00000000..1ad9b4bc --- /dev/null +++ b/musify/log/__init__.py @@ -0,0 +1,18 @@ +""" +All classes and functions pertaining to logging operations throughout the package. +""" +import logging + +LOGGING_DT_FORMAT = "%Y-%m-%d_%H.%M.%S" + +INFO_EXTRA = logging.INFO - 1 +logging.addLevelName(INFO_EXTRA, "INFO_EXTRA") +logging.INFO_EXTRA = INFO_EXTRA + +REPORT = logging.INFO - 3 +logging.addLevelName(REPORT, "REPORT") +logging.REPORT = REPORT + +STAT = logging.DEBUG + 3 +logging.addLevelName(STAT, "STAT") +logging.STAT = STAT diff --git a/musify/log/filter.py b/musify/log/filter.py new file mode 100644 index 00000000..6fb61db8 --- /dev/null +++ b/musify/log/filter.py @@ -0,0 +1,87 @@ +""" +All logging filters specific to this package. +""" +import inspect +import logging.handlers +import re +from os.path import join, splitext, split, basename + +from musify import PROGRAM_NAME + + +def format_full_func_name(record: logging.LogRecord, width: int = 40) -> None: + """ + Set fully qualified path name to function including class name to the given record. + Optionally, provide a max ``width`` to attempt to truncate the path name to + by taking only the first letter of each part of the path until the length is equal to ``width``. + """ + last_call = inspect.stack()[8] + + if record.pathname == __file__: + # custom logging method has been called, reformat call info to actual call method + record.pathname = last_call.filename + record.lineno = last_call.lineno + record.funcName = last_call.function + record.filename = basename(record.pathname) + record.module = record.name.split(".")[-1] + + f_locals = last_call.frame.f_locals + if "self" not in f_locals: + path_split = record.name.split(".") + if record.funcName != "": + path_split.append(record.funcName) + else: + # is a valid and initialised object, extract the class name and determine path to call function from stack + cls = f_locals["self"].__class__ + path = join(splitext(inspect.getfile(cls))[0], cls.__name__, record.funcName.split(".")[-1]) + + folder = "" + path_split = [] + while not folder.casefold().startswith(PROGRAM_NAME.casefold()) and path: # get relative path to sources root + path, folder = split(path) + path_split.append(folder) + path_split.append(PROGRAM_NAME.lower()) + + # produce fully qualified path + path_split = list(reversed(path_split[:-1])) + + # truncate long paths by taking first letters of each part until short enough + path = ".".join(path_split) + for i, part in enumerate(path_split): + if len(path) <= width: + break + if not part: + continue + + # take all upper case characters if they exist in part, else, if all lower case, take first letter + path_split[i] = re.sub("[a-z_]+", "", part) if re.match("[A-Z]", part) else part[0] + path = ".".join(path_split) + + record.funcName = path + + +class LogConsoleFilter(logging.Filter): + """Filter for logging to the console.""" + + def __init__(self, name: str = "", module_width: int = 40): + super().__init__(name) + self.module_width = module_width + + # noinspection PyMissingOrEmptyDocstring + def filter(self, record: logging.LogRecord) -> logging.LogRecord | None: + format_full_func_name(record, width=self.module_width) + return record + + +class LogFileFilter(logging.Filter): + """Filter for logging to a file.""" + + def __init__(self, name: str = "", module_width: int = 40): + super().__init__(name) + self.module_width = module_width + + # noinspection PyMissingOrEmptyDocstring + def filter(self, record: logging.LogRecord) -> logging.LogRecord: + record.msg = re.sub("\33.*?m", "", record.msg) + format_full_func_name(record, width=self.module_width) + return record diff --git a/musify/shared/handlers.py b/musify/log/handlers.py similarity index 92% rename from musify/shared/handlers.py rename to musify/log/handlers.py index 9cc28ed8..91c69259 100644 --- a/musify/shared/handlers.py +++ b/musify/log/handlers.py @@ -1,7 +1,6 @@ """ All logging handlers specific to this package. """ - import logging.handlers import os import shutil @@ -9,13 +8,10 @@ from glob import glob from os.path import join, dirname, isfile, sep, isdir +from musify.log import LOGGING_DT_FORMAT from musify.processors.time import TimeMapper -from musify.shared.logger import LOGGING_DT_FORMAT -########################################################################### -## Logging handlers -########################################################################### class CurrentTimeRotatingFileHandler(logging.handlers.BaseRotatingHandler): """ Handles log file and directory rotation based on log file/folder name. diff --git a/musify/shared/logger.py b/musify/log/logger.py similarity index 51% rename from musify/shared/logger.py rename to musify/log/logger.py index f9578af3..7370335d 100644 --- a/musify/shared/logger.py +++ b/musify/log/logger.py @@ -1,38 +1,17 @@ """ -All classes and operations (apart from handlers) relating to loggers for the entire package. +All classes and operations relating to the logger objects used throughout the entire package. """ - -import inspect import logging import logging.config import logging.handlers import os -import re import sys from collections.abc import Iterable -from os.path import join, splitext, split, basename from typing import Any from tqdm.auto import tqdm -from musify import PROGRAM_NAME - -LOGGING_DT_FORMAT = "%Y-%m-%d_%H.%M.%S" - -########################################################################### -## Setup default Logger with extended levels -########################################################################### -INFO_EXTRA = logging.INFO - 1 -logging.addLevelName(INFO_EXTRA, "INFO_EXTRA") -logging.INFO_EXTRA = INFO_EXTRA - -REPORT = logging.INFO - 3 -logging.addLevelName(REPORT, "REPORT") -logging.REPORT = REPORT - -STAT = logging.DEBUG + 3 -logging.addLevelName(STAT, "STAT") -logging.STAT = STAT +from musify.log import INFO_EXTRA, REPORT, STAT class MusifyLogger(logging.Logger): @@ -142,84 +121,3 @@ def __deepcopy__(self, _: dict = None): logging.setLoggerClass(MusifyLogger) - - -########################################################################### -## Logging formatters/filters -########################################################################### -def format_full_func_name(record: logging.LogRecord, width: int = 40) -> None: - """ - Set fully qualified path name to function including class name to the given record. - Optionally, provide a max ``width`` to attempt to truncate the path name to - by taking only the first letter of each part of the path until the length is equal to ``width``. - """ - last_call = inspect.stack()[8] - - if record.pathname == __file__: - # custom logging method has been called, reformat call info to actual call method - record.pathname = last_call.filename - record.lineno = last_call.lineno - record.funcName = last_call.function - record.filename = basename(record.pathname) - record.module = record.name.split(".")[-1] - - f_locals = last_call.frame.f_locals - if "self" not in f_locals: - path_split = record.name.split(".") - if record.funcName != "": - path_split.append(record.funcName) - else: - # is a valid and initialised object, extract the class name and determine path to call function from stack - cls = f_locals["self"].__class__ - path = join(splitext(inspect.getfile(cls))[0], cls.__name__, record.funcName.split(".")[-1]) - - folder = "" - path_split = [] - while not folder.casefold().startswith(PROGRAM_NAME.casefold()) and path: # get relative path to sources root - path, folder = split(path) - path_split.append(folder) - path_split.append(PROGRAM_NAME.lower()) - - # produce fully qualified path - path_split = list(reversed(path_split[:-1])) - - # truncate long paths by taking first letters of each part until short enough - path = ".".join(path_split) - for i, part in enumerate(path_split): - if len(path) <= width: - break - if not part: - continue - - # take all upper case characters if they exist in part, else, if all lower case, take first letter - path_split[i] = re.sub("[a-z_]+", "", part) if re.match("[A-Z]", part) else part[0] - path = ".".join(path_split) - - record.funcName = path - - -class LogConsoleFilter(logging.Filter): - """Filter for logging to the console.""" - - def __init__(self, name: str = "", module_width: int = 40): - super().__init__(name) - self.module_width = module_width - - # noinspection PyMissingOrEmptyDocstring - def filter(self, record: logging.LogRecord) -> logging.LogRecord | None: - format_full_func_name(record, width=self.module_width) - return record - - -class LogFileFilter(logging.Filter): - """Filter for logging to a file.""" - - def __init__(self, name: str = "", module_width: int = 40): - super().__init__(name) - self.module_width = module_width - - # noinspection PyMissingOrEmptyDocstring - def filter(self, record: logging.LogRecord) -> logging.LogRecord: - record.msg = re.sub("\33.*?m", "", record.msg) - format_full_func_name(record, width=self.module_width) - return record diff --git a/musify/processors/base.py b/musify/processors/base.py index 25024313..c164f6b7 100644 --- a/musify/processors/base.py +++ b/musify/processors/base.py @@ -1,17 +1,16 @@ """ Base classes for all processors in this module. Also contains decorators for use in implementations. """ - import logging from abc import ABCMeta, abstractmethod from collections.abc import Mapping, Callable, Collection, Iterable, MutableSequence from functools import partial, update_wrapper from typing import Any, Self, Optional +from musify.core.printer import PrettyPrinter +from musify.log.logger import MusifyLogger from musify.processors.exception import ProcessorLookupError -from musify.shared.core.misc import PrettyPrinter -from musify.shared.logger import MusifyLogger -from musify.shared.utils import get_user_input, get_max_width, align_string +from musify.utils import get_user_input, get_max_width, align_string class Processor(PrettyPrinter, metaclass=ABCMeta): @@ -51,11 +50,11 @@ def _format_help_text(options: Mapping[str, str], header: MutableSequence[str] | class ItemProcessor(Processor, metaclass=ABCMeta): - """Base object for processing :py:class:`Item` objects""" + """Base object for processing :py:class:`MusifyItem` objects""" class MusicBeeProcessor(ItemProcessor): - """Base object for processing :py:class:`Item` objects on MusicBee settings""" + """Base object for processing :py:class:`MusifyItem` objects on MusicBee settings""" @classmethod def _processor_method_fmt(cls, name: str) -> str: @@ -203,7 +202,7 @@ def process(self, values: Collection[T], *args, **kwargs) -> Collection[T]: def transform(self) -> Callable[[Any], Any]: """ Transform the input ``value`` to the value that should be used when comparing against this filter's settings - Simply returns the given ``value`` at baseline unless overriden. + Simply returns the given ``value`` at baseline unless overridden. """ return self._transform diff --git a/musify/processors/compare.py b/musify/processors/compare.py index 46019669..8888a1b2 100644 --- a/musify/processors/compare.py +++ b/musify/processors/compare.py @@ -1,7 +1,6 @@ """ Processor making comparisons between objects and data types. """ - import re from collections.abc import Mapping, Sequence from datetime import datetime, date @@ -9,15 +8,15 @@ from operator import mul from typing import Any, Self +from musify.core.base import MusifyItem +from musify.core.enum import Field +from musify.exception import FieldError +from musify.field import Fields from musify.processors.base import DynamicProcessor, MusicBeeProcessor, dynamicprocessormethod from musify.processors.exception import ComparerError from musify.processors.time import TimeMapper -from musify.shared.core.base import MusifyItem -from musify.shared.core.enum import Field -from musify.shared.exception import FieldError -from musify.shared.field import Fields -from musify.shared.types import UnitSequence -from musify.shared.utils import to_collection +from musify.types import UnitSequence +from musify.utils import to_collection # Map of MusicBee field name to Field enum # noinspection SpellCheckingInspection @@ -115,14 +114,14 @@ def __init__(self, condition: str, expected: UnitSequence[Any] | None = None, fi self._converted = False #: The :py:class:`Field` representing the property to extract the comparison value from - #: when an :py:class:`Item` is given + #: when an :py:class:`MusifyItem` is given self.field: Field | None = field.map(field)[0] if field else None self.expected: list[Any] | None = to_collection(expected, list) self._set_processor_name(condition) - def __call__[T: Any](self, item: T, reference: UnitSequence[T] | None = None) -> bool: - return self.compare(item=item, reference=reference) + def __call__(self, *args, **kwargs) -> bool: + return self.compare(*args, **kwargs) def compare[T: Any](self, item: T, reference: T | None = None) -> bool: """ diff --git a/musify/processors/download.py b/musify/processors/download.py index 3caba441..875127bc 100644 --- a/musify/processors/download.py +++ b/musify/processors/download.py @@ -1,16 +1,19 @@ +""" +Processor that helps user download songs from collections based on given configuration. +""" import re from collections.abc import Iterable, Collection from itertools import batched from typing import Any from webbrowser import open as webopen +from musify.core.base import MusifyItem +from musify.core.enum import Field, Fields +from musify.exception import MusifyEnumError +from musify.libraries.core.collection import MusifyCollection from musify.processors.base import InputProcessor, ItemProcessor -from musify.shared.core.base import MusifyItem -from musify.shared.core.collection import MusifyCollection -from musify.shared.core.enum import Field, Fields -from musify.shared.exception import MusifyEnumError -from musify.shared.types import UnitIterable -from musify.shared.utils import to_collection +from musify.types import UnitIterable +from musify.utils import to_collection class ItemDownloadHelper(InputProcessor, ItemProcessor): @@ -33,8 +36,8 @@ def __init__(self, urls: UnitIterable[str], fields: UnitIterable[Field] = Fields self.fields: list[Field] = to_collection(fields, list) self.interval = interval - def __call__(self, collections: UnitIterable[MusifyCollection]) -> None: - self.open_sites(collections=collections) + def __call__(self, *args, **kwargs) -> None: + return self.open_sites(*args, **kwargs) def open_sites(self, collections: UnitIterable[MusifyCollection]) -> None: """ diff --git a/musify/processors/exception.py b/musify/processors/exception.py index 81ce9ce1..9d3a5605 100644 --- a/musify/processors/exception.py +++ b/musify/processors/exception.py @@ -1,8 +1,7 @@ """ Exceptions relating to processor operations. """ - -from musify.shared.exception import MusifyError +from musify.exception import MusifyError class ProcessorError(MusifyError): diff --git a/musify/processors/filter.py b/musify/processors/filter.py index b21bfb74..4cddb117 100644 --- a/musify/processors/filter.py +++ b/musify/processors/filter.py @@ -1,15 +1,14 @@ """ Processors that filter down objects and data types based on some given configuration. """ - from __future__ import annotations from collections.abc import Collection, Sequence, Mapping from typing import Any, Self +from musify.core.base import MusifyObject from musify.processors.base import Filter, FilterComposite from musify.processors.compare import Comparer -from musify.shared.core.base import MusifyObject class FilterDefinedList[T: str | MusifyObject](Filter[T], Collection[T]): @@ -25,8 +24,8 @@ def __init__(self, values: Collection[T] = (), *_, **__): #: The values to include when processing for this filter self.values: Collection[T] = values - def __call__(self, values: Collection[T] | None = None, *_, **__) -> Collection[T]: - return self.process(values=values) + def __call__(self, *args, **kwargs) -> Collection[T]: + return self.process(*args, **kwargs) def process(self, values: Collection[T] | None = None, *_, **__) -> Collection[T]: """Returns all ``values`` that match this filter's settings""" @@ -77,8 +76,8 @@ def __init__( #: When true, only include those items that match on all comparers self.match_all: bool = match_all - def __call__(self, values: Collection[T], reference: T | None = None, *_, **__) -> Collection[T]: - return self.process(values=values, reference=reference) + def __call__(self, *args, **kwargs) -> Collection[T]: + return self.process(*args, **kwargs) def process(self, values: Collection[T], reference: T | None = None, *_, **__) -> Collection[T]: if not self.ready: @@ -130,8 +129,8 @@ def __init__(self, include: U, exclude: V, *_, **__): #: The filter that, when processed, returns items to exclude self.exclude: V = exclude - def __call__(self, values: Collection[T], *_, **__) -> list[T]: - return self.process(values=values) + def __call__(self, *args, **kwargs) -> list[T]: + return self.process(*args, **kwargs) def process(self, values: Collection[T], *_, **__) -> list[T]: """Filter down ``values`` that match this filter's settings from""" diff --git a/musify/processors/filter_matcher.py b/musify/processors/filter_matcher.py index d3ddbea6..a15dbc1c 100644 --- a/musify/processors/filter_matcher.py +++ b/musify/processors/filter_matcher.py @@ -1,7 +1,6 @@ """ Processors that filter down objects and data types based on some given configuration. """ - from __future__ import annotations import logging @@ -9,16 +8,17 @@ from dataclasses import field, dataclass from typing import Any -from musify.local.track import LocalTrack +from musify.core.base import MusifyItem +from musify.core.enum import Fields +from musify.core.result import Result +from musify.file.base import File +from musify.file.path_mapper import PathMapper +from musify.log.logger import MusifyLogger from musify.processors.base import Filter, MusicBeeProcessor, FilterComposite from musify.processors.compare import Comparer from musify.processors.filter import FilterComparers, FilterDefinedList from musify.processors.sort import ItemSorter -from musify.shared.core.enum import Fields -from musify.shared.core.misc import Result -from musify.shared.file import File, PathMapper -from musify.shared.logger import MusifyLogger -from musify.shared.utils import to_collection +from musify.utils import to_collection @dataclass(frozen=True) @@ -104,8 +104,8 @@ def from_xml( def to_xml( self, - items: list[LocalTrack], - original: list[LocalTrack], + items: list[File], + original: list[File | MusifyItem], path_mapper: Callable[[Collection[str | File]], Collection[str]] = lambda x: x, **__ ) -> Mapping[str, Any]: @@ -123,7 +123,7 @@ def to_xml( ) return {} - output_path_map: Mapping[str, LocalTrack] = {item.path.casefold(): item for item in items} + output_path_map: Mapping[str, File] = {item.path.casefold(): item for item in items} if self.comparers: # match again on current conditions to check for differences from original list @@ -176,8 +176,8 @@ def __init__( #: The filter that, when processed, returns items to exclude self.exclude = exclude - def __call__(self, values: Collection[T], reference: T | None = None, *_, **__) -> list[T]: - return self.process(values=values, reference=reference) + def __call__(self, *args, **kwargs) -> list[T]: + return self.process(*args, **kwargs) def process(self, values: Collection[T], reference: T | None = None, *_, **__) -> list[T]: """ diff --git a/musify/processors/limit.py b/musify/processors/limit.py index ee9d6e19..714d8fe4 100644 --- a/musify/processors/limit.py +++ b/musify/processors/limit.py @@ -1,20 +1,19 @@ """ Processor that limits the items in a given collection of items """ - from collections.abc import Collection, Mapping from functools import reduce from operator import mul from random import shuffle from typing import Any, Self +from musify.core.base import MusifyItem +from musify.core.enum import MusifyEnum, Fields +from musify.file.base import File +from musify.libraries.core.object import Track from musify.processors.base import DynamicProcessor, MusicBeeProcessor, dynamicprocessormethod from musify.processors.exception import ItemLimiterError from musify.processors.sort import ItemSorter -from musify.shared.core.base import MusifyItem -from musify.shared.core.enum import MusifyEnum, Fields -from musify.shared.core.object import Track -from musify.shared.file import File class LimitType(MusifyEnum): @@ -96,8 +95,8 @@ def __init__( self._set_processor_name(sorted_by, fail_on_empty=False) - def __call__[T: MusifyItem](self, items: list[T], ignore: Collection[T] = ()) -> None: - return self.limit(items=items, ignore=ignore) + def __call__(self, *args, **kwargs) -> None: + return self.limit(*args, **kwargs) def limit[T: MusifyItem](self, items: list[T], ignore: Collection[T] = ()) -> None: """ diff --git a/musify/processors/match.py b/musify/processors/match.py index dc5eeb15..92e337be 100644 --- a/musify/processors/match.py +++ b/musify/processors/match.py @@ -1,7 +1,6 @@ """ Processor that matches objects and data types based on given configuration. """ - import inspect import logging import re @@ -9,15 +8,15 @@ from dataclasses import dataclass, field from typing import Any +from musify.core.base import MusifyObject +from musify.core.enum import TagField, TagFields as Tag, ALL_TAG_FIELDS +from musify.core.printer import PrettyPrinter +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.core.object import Track, Album +from musify.log.logger import MusifyLogger from musify.processors.base import ItemProcessor -from musify.shared.core.base import MusifyObject -from musify.shared.core.collection import MusifyCollection -from musify.shared.core.enum import TagField, TagFields as Tag, ALL_TAG_FIELDS -from musify.shared.core.misc import PrettyPrinter -from musify.shared.core.object import Track, Album -from musify.shared.logger import MusifyLogger -from musify.shared.types import UnitIterable -from musify.shared.utils import limit_value, to_collection +from musify.types import UnitIterable +from musify.utils import limit_value, to_collection @dataclass @@ -71,11 +70,11 @@ class ItemMatcher(ItemProcessor): #: A set of words to check for when applying name score reduction logic. #: #: If a word from this list is present in the name of the result to score against - #: but not in the source :py:class:`Item`, apply the ``reduce_name_score_factor`` to reduce its score. + #: but not in the source :py:class:`MusifyItem`, apply the ``reduce_name_score_factor`` to reduce its score. #: This set is always combined with the ``karaoke_tags``. reduce_name_score_on = {"live", "demo", "acoustic"} #: The factor to apply to a name score when a word from ``reduce_name_score_on`` - #: is found in the result but not in the source :py:class:`Item`. + #: is found in the result but not in the source :py:class:`MusifyItem`. reduce_name_score_factor = 0.5 def __init__(self): @@ -263,23 +262,8 @@ def match_year[T: (Track, Album)](self, source: T, result: T) -> float: ########################################################################### ## Score match ########################################################################### - def __call__[T: (Track, Album)]( - self, - source: T, - results: Iterable[T], - min_score: float = 0.1, - max_score: float = 0.8, - match_on: UnitIterable[TagField] = ALL_TAG_FIELDS, - allow_karaoke: bool = False, - ) -> T | None: - return self.match( - source=source, - results=results, - min_score=min_score, - max_score=max_score, - match_on=match_on, - allow_karaoke=allow_karaoke - ) + def __call__[T: (Track, Album)](self, *args, **kwargs) -> T | None: + return self.match(*args, **kwargs) def match[T: (Track, Album)]( self, @@ -387,7 +371,8 @@ def _get_scores[T: (Track, Album)]( ) -> dict[TagField, float]: """ Gets the scores from a cleaned source and result to match on. - When an ItemCollection is given to match on, scores are also calculated for each of the items in the collection. + When an MusifyCollection is given to match on, + scores are also calculated for each of the items in the collection. Scores are always between 0-1. :param source: Source item to compare against and find a match for with assigned ``clean_tags``. diff --git a/musify/processors/sort.py b/musify/processors/sort.py index c6395676..3b69a485 100644 --- a/musify/processors/sort.py +++ b/musify/processors/sort.py @@ -1,18 +1,17 @@ """ Processor that sorts the given collection of items based on given configuration. """ - from collections.abc import Callable, Mapping, MutableMapping, Sequence, MutableSequence, Iterable from copy import copy from datetime import datetime from random import shuffle from typing import Any, Self +from musify.core.base import MusifyItem +from musify.core.enum import MusifyEnum, Field, Fields from musify.processors.base import MusicBeeProcessor -from musify.shared.core.base import MusifyItem -from musify.shared.core.enum import MusifyEnum, Field, Fields -from musify.shared.types import UnitSequence, UnitIterable -from musify.shared.utils import flatten_nested, strip_ignore_words, to_collection, limit_value +from musify.types import UnitSequence, UnitIterable +from musify.utils import flatten_nested, strip_ignore_words, to_collection, limit_value class ShuffleMode(MusifyEnum): @@ -183,8 +182,8 @@ def __init__( self.shuffle_by: ShuffleBy | None = shuffle_by self.shuffle_weight = limit_value(shuffle_weight, floor=-1, ceil=1) - def __call__(self, items: MutableSequence[MusifyItem]) -> None: - return self.sort(items=items) + def __call__(self, *args, **kwargs) -> None: + return self.sort(*args, **kwargs) def sort(self, items: MutableSequence[MusifyItem]) -> None: """Sorts a list of ``items`` in-place.""" diff --git a/musify/processors/time.py b/musify/processors/time.py index e45e0923..a17c5683 100644 --- a/musify/processors/time.py +++ b/musify/processors/time.py @@ -1,14 +1,13 @@ """ Processor that converts representations of time units to python time objects. """ - from datetime import timedelta from typing import Any from dateutil.relativedelta import relativedelta +from musify.core.printer import PrettyPrinter from musify.processors.base import DynamicProcessor, dynamicprocessormethod -from musify.shared.core.misc import PrettyPrinter class TimeMapper(DynamicProcessor, PrettyPrinter): @@ -22,8 +21,8 @@ def __init__(self, func: str): super().__init__() self._set_processor_name(func) - def __call__(self, value: Any): - return self.map(value) + def __call__(self, *args, **kwargs): + return self.map(*args, **kwargs) def map(self, value: Any): """Run the mapping function""" diff --git a/musify/report.py b/musify/report.py index d933f0c0..d744da2a 100644 --- a/musify/report.py +++ b/musify/report.py @@ -1,18 +1,18 @@ """ Meta-functions for providing reports to the user based on comparisons between objects implemented in this package. """ - import logging from collections.abc import Iterable -from musify.local.library import LocalLibrary -from musify.shared.core.base import MusifyItem -from musify.shared.core.collection import MusifyCollection -from musify.shared.core.enum import TagField, Fields, ALL_FIELDS, TagFields -from musify.shared.core.object import Library, Playlist -from musify.shared.logger import MusifyLogger, REPORT -from musify.shared.types import UnitIterable -from musify.shared.utils import align_string, get_max_width, to_collection +from musify.core.base import MusifyItem +from musify.core.enum import TagField, Fields, ALL_FIELDS, TagFields +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.core.object import Library, Playlist +from musify.libraries.local.library import LocalLibrary +from musify.log import REPORT +from musify.log.logger import MusifyLogger +from musify.types import UnitIterable +from musify.utils import align_string, get_max_width, to_collection def report_playlist_differences( diff --git a/musify/shared/__init__.py b/musify/shared/__init__.py deleted file mode 100644 index c80722ec..00000000 --- a/musify/shared/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -All shared abstract classes, utility functions, exceptions, and logger functionality for the entire package. -""" diff --git a/musify/shared/core/__init__.py b/musify/shared/core/__init__.py deleted file mode 100644 index 4b6e36a4..00000000 --- a/musify/shared/core/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -All core abstract classes for the entire package. - -This module contains base classes (non-exhaustive): - * :py:class:`Nameable`, :py:class:`Taggable`, :py:class:`Item` - * :py:class:`ItemCollection` - * :py:class:`MusifyEnum`, :py:class:`Fields` (including complete :py:class:`Field` enum implementations - for all :py:class:`Field` types) - * :py:class:`PrettyPrinter`, :py:class:`Result` - * All Musify object types including :py:class:`Track`, :py:class:`Playlist`, - :py:class:`Album`, :py:class:`Artist` etc. -""" diff --git a/musify/shared/exception.py b/musify/shared/exception.py deleted file mode 100644 index c89b1b8e..00000000 --- a/musify/shared/exception.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -Core exceptions for the entire package. -""" - -from typing import Any - - -class MusifyError(Exception): - """Generic base class for all Musify-related errors""" - - -class MusifyKeyError(MusifyError, KeyError): - """Exception raised for invalid keys.""" - - -class MusifyValueError(MusifyError, ValueError): - """Exception raised for invalid values.""" - - -class MusifyTypeError(MusifyError, TypeError): - """Exception raised for invalid types.""" - def __init__(self, kind: Any, message: str = "Invalid item type given"): - self.message = message - super().__init__(f"{self.message}: {kind}") - - -class MusifyAttributeError(MusifyError, AttributeError): - """Exception raised for invalid attributes.""" - - -########################################################################### -## Enum errors -########################################################################### -class MusifyEnumError(MusifyError): - """ - Exception raised for errors related to :py:class:`MusifyEnum` implementations. - - :param value: The value that caused the error. - :param message: Explanation of the error. - """ - - def __init__(self, value: Any, message: str = "Could not find enum"): - self.message = message - super().__init__(f"{self.message}: {value}") - - -class FieldError(MusifyEnumError): - """ - Exception raised for errors related to :py:class:`Field` enums. - - :param message: Explanation of the error. - """ - def __init__(self, message: str | None = None, field: Any | None = None): - super().__init__(value=field, message=message) - - -########################################################################### -## File errors -########################################################################### -class FileError(MusifyError): - """ - Exception raised for file errors. - - :param file: The file type that caused the error. - :param message: Explanation of the error. - """ - - def __init__(self, file: str | None = None, message: str | None = None): - self.file = file - self.message = message - formatted = f"{file} | {message}" if file else message - super().__init__(formatted) - - -class InvalidFileType(FileError): - """ - Exception raised for unrecognised file types. - - :param filetype: The file type that caused the error. - :param message: Explanation of the error. - """ - - def __init__(self, filetype: str, message: str = "File type not recognised"): - self.filetype = filetype - self.message = message - super().__init__(file=filetype, message=message) - - -class FileDoesNotExistError(FileError, FileNotFoundError): - """ - Exception raised when a file cannot be found. - - :param path: The path that caused the error. - :param message: Explanation of the error. - """ - - def __init__(self, path: str, message: str = "File cannot be found"): - self.path = path - self.message = message - super().__init__(file=path, message=message) - - -class ImageLoadError(FileError): - """Exception raised for errors in loading an image.""" diff --git a/musify/shared/types.py b/musify/types.py similarity index 56% rename from musify/shared/types.py rename to musify/types.py index fa2b6872..dccf91ed 100644 --- a/musify/shared/types.py +++ b/musify/types.py @@ -1,7 +1,6 @@ """ All core type hints to use throughout the entire package. """ - from collections.abc import Iterable, Sequence, MutableSequence, Collection, Mapping, MutableMapping from typing import TypeVar @@ -12,9 +11,9 @@ UnitMutableSequence = UT | MutableSequence[UT] UnitList = UT | list[UT] -JSON = Mapping[str, str | int | float | list | dict | bool | None] -MutableJSON = MutableMapping[str, str | int | float | list | dict | bool | None] -UnitJSON = UnitMutableSequence[str] | UnitMutableSequence[JSON] -UnitMutableJSON = UnitMutableSequence[str] | UnitMutableSequence[MutableJSON] +JSON_VALUE = str | int | float | list | dict | bool | None +JSON = Mapping[str, JSON_VALUE] +MutableJSON = MutableMapping[str, JSON_VALUE] +DictJSON = dict[str, JSON_VALUE] Number = int | float diff --git a/musify/shared/utils.py b/musify/utils.py similarity index 96% rename from musify/shared/utils.py rename to musify/utils.py index 0a9eafde..038f8085 100644 --- a/musify/shared/utils.py +++ b/musify/utils.py @@ -1,7 +1,6 @@ """ Generic utility functions and classes which can be used throughout the entire package. """ - import re from collections import Counter from collections.abc import Iterable, Collection, MutableSequence, Mapping, MutableMapping @@ -9,8 +8,8 @@ import unicodedata -from musify.shared.exception import MusifyTypeError -from musify.shared.types import Number +from musify.exception import MusifyTypeError +from musify.types import Number class SafeDict(dict): diff --git a/readme.py b/readme.py index 17150669..b9b84114 100644 --- a/readme.py +++ b/readme.py @@ -1,13 +1,12 @@ """ Fills in the variable fields of the README template and generates README.md file. """ - from musify import PROGRAM_OWNER_USER, PROGRAM_NAME -from musify.local.track import TRACK_FILETYPES -from musify.local.playlist import PLAYLIST_FILETYPES -from musify.local.library import LIBRARY_CLASSES, LocalLibrary -from musify.shared.utils import SafeDict -from musify.spotify.processors import SpotifyDataWrangler +from musify.libraries.local.track import TRACK_FILETYPES +from musify.libraries.local.playlist import PLAYLIST_FILETYPES +from musify.libraries.local.library import LIBRARY_CLASSES, LocalLibrary +from musify.utils import SafeDict +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler SRC_FILENAME = "README.template.md" TRG_FILENAME = SRC_FILENAME.replace(".template", "") diff --git a/tests/__resources/test_logging.yml b/tests/__resources/test_logging.yml index 64e34b8b..293207c0 100644 --- a/tests/__resources/test_logging.yml +++ b/tests/__resources/test_logging.yml @@ -16,10 +16,10 @@ formatters: filters: console: - (): "musify.shared.logger.LogConsoleFilter" + (): "musify.log.filter.LogConsoleFilter" module_width: 40 file: - (): "musify.shared.logger.LogFileFilter" + (): "musify.log.filter.LogFileFilter" module_width: 40 handlers: @@ -43,7 +43,7 @@ handlers: level: INFO_EXTRA file: - class: musify.shared.handlers.CurrentTimeRotatingFileHandler + class: musify.log.handlers.CurrentTimeRotatingFileHandler level: DEBUG formatter: extended filters: ["file"] diff --git a/tests/local/__init__.py b/tests/api/__init__.py similarity index 100% rename from tests/local/__init__.py rename to tests/api/__init__.py diff --git a/tests/shared/api/test_authorise.py b/tests/api/test_authorise.py similarity index 96% rename from tests/shared/api/test_authorise.py rename to tests/api/test_authorise.py index 0bfcba12..d22d9a87 100644 --- a/tests/shared/api/test_authorise.py +++ b/tests/api/test_authorise.py @@ -12,9 +12,9 @@ from requests_mock import Mocker from musify import MODULE_ROOT -from musify.shared.api.authorise import APIAuthoriser -from musify.shared.api.exception import APIError -from tests.shared.api.utils import path_token +from musify.api.authorise import APIAuthoriser +from musify.api.exception import APIError +from tests.api.utils import path_token class TestAPIAuthoriser: @@ -111,7 +111,7 @@ def check_url(url: str): socket_listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) requests_mock.post(user_url) - mocker.patch(f"{MODULE_ROOT}.shared.api.authorise.webopen", new=check_url) + mocker.patch(f"{MODULE_ROOT}.api.authorise.webopen", new=check_url) mocker.patch.object(socket.socket, attribute="accept", return_value=(socket_listener, None)) mocker.patch.object(socket.socket, attribute="send") mocker.patch.object(socket.socket, attribute="recv", return_value=response.encode("utf-8")) diff --git a/tests/shared/api/test_request.py b/tests/api/test_request.py similarity index 96% rename from tests/shared/api/test_request.py rename to tests/api/test_request.py index f4b52853..36bf80db 100644 --- a/tests/shared/api/test_request.py +++ b/tests/api/test_request.py @@ -14,8 +14,8 @@ # noinspection PyProtectedMember,PyUnresolvedReferences from requests_mock.response import _Context as Context -from musify.shared.api.exception import APIError -from musify.shared.api.request import RequestHandler +from musify.api.exception import APIError +from musify.api.request import RequestHandler class TestRequestHandler: diff --git a/tests/shared/api/utils.py b/tests/api/utils.py similarity index 100% rename from tests/shared/api/utils.py rename to tests/api/utils.py diff --git a/tests/conftest.py b/tests/conftest.py index ec1f9403..d07a7cd2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,13 +13,13 @@ from _pytest.fixtures import SubRequest from musify import MODULE_ROOT -from musify.shared.api.request import RequestHandler -from musify.shared.logger import MusifyLogger -from musify.shared.remote.enum import RemoteObjectType -from musify.spotify.api import SpotifyAPI -from musify.spotify.processors import SpotifyDataWrangler -from tests.shared.remote.utils import ALL_ITEM_TYPES -from tests.spotify.api.mock import SpotifyMock +from musify.api.request import RequestHandler +from musify.log.logger import MusifyLogger +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from tests.libraries.remote.core.utils import ALL_ITEM_TYPES +from tests.libraries.remote.spotify.api.mock import SpotifyMock from tests.utils import idfn diff --git a/tests/local/library/__init__.py b/tests/core/__init__.py similarity index 100% rename from tests/local/library/__init__.py rename to tests/core/__init__.py diff --git a/tests/shared/core/base.py b/tests/core/base.py similarity index 60% rename from tests/shared/core/base.py rename to tests/core/base.py index 2963fe4c..dbdbe13a 100644 --- a/tests/shared/core/base.py +++ b/tests/core/base.py @@ -2,27 +2,30 @@ import pytest -from musify.shared.core.base import MusifyItem -from musify.shared.core.misc import PrettyPrinter -from tests.shared.core.misc import PrettyPrinterTester +from musify.core.base import MusifyItem +from musify.core.printer import PrettyPrinter +from tests.core.printer import PrettyPrinterTester -class ItemTester(PrettyPrinterTester, metaclass=ABCMeta): - """Run generic tests for :py:class:`Item` implementations""" +class MusifyItemTester(PrettyPrinterTester, metaclass=ABCMeta): + """Run generic tests for :py:class:`MusifyItem` implementations""" @abstractmethod def item(self, *args, **kwargs) -> MusifyItem: - """Yields an :py:class:`Item` object to be tested as pytest.fixture""" + """Yields an :py:class:`MusifyItem` object to be tested as pytest.fixture""" raise NotImplementedError @abstractmethod def item_unequal(self, *args, **kwargs) -> MusifyItem: - """Yields an :py:class:`Item` object that is does not equal the ``item`` being tested""" + """Yields an :py:class:`MusifyItem` object that is does not equal the ``item`` being tested""" raise NotImplementedError @abstractmethod def item_modified(self, *args, **kwargs) -> MusifyItem: - """Yields an :py:class:`Item` object that is equal to the ``item`` being tested with some modified values""" + """ + Yields an :py:class:`MusifyItem` object that is equal to the ``item`` + being tested with some modified values + """ raise NotImplementedError @pytest.fixture diff --git a/tests/shared/core/enums.py b/tests/core/enum.py similarity index 93% rename from tests/shared/core/enums.py rename to tests/core/enum.py index a03c1adc..157fd72a 100644 --- a/tests/shared/core/enums.py +++ b/tests/core/enum.py @@ -1,8 +1,8 @@ from abc import ABC, abstractmethod, ABCMeta from collections.abc import Container -from musify.shared.core.base import MusifyObject -from musify.shared.core.enum import MusifyEnum, Fields, TagField, ALL_FIELDS, Field +from musify.core.base import MusifyObject +from musify.core.enum import MusifyEnum, Fields, TagField, ALL_FIELDS, Field class EnumTester(ABC): diff --git a/tests/shared/core/misc.py b/tests/core/printer.py similarity index 93% rename from tests/shared/core/misc.py rename to tests/core/printer.py index e99a4ea7..9dcb9a8d 100644 --- a/tests/shared/core/misc.py +++ b/tests/core/printer.py @@ -2,7 +2,7 @@ import re from abc import ABC, abstractmethod -from musify.shared.core.misc import PrettyPrinter +from musify.core.printer import PrettyPrinter class PrettyPrinterTester(ABC): diff --git a/tests/local/playlist/__init__.py b/tests/libraries/__init__.py similarity index 100% rename from tests/local/playlist/__init__.py rename to tests/libraries/__init__.py diff --git a/tests/local/track/__init__.py b/tests/libraries/core/__init__.py similarity index 100% rename from tests/local/track/__init__.py rename to tests/libraries/core/__init__.py diff --git a/tests/shared/core/collection.py b/tests/libraries/core/collection.py similarity index 85% rename from tests/shared/core/collection.py rename to tests/libraries/core/collection.py index 5aa550c2..241a55af 100644 --- a/tests/shared/core/collection.py +++ b/tests/libraries/core/collection.py @@ -4,20 +4,21 @@ import pytest +from musify.core.base import MusifyItem +from musify.core.printer import PrettyPrinter +from musify.exception import MusifyTypeError +from musify.libraries.collection import BasicCollection +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.core.object import Library, Playlist +from musify.libraries.remote.core.library import RemoteLibrary +from musify.libraries.remote.core.object import RemoteCollectionLoader from musify.processors.filter import FilterDefinedList, FilterIncludeExclude -from musify.shared.core.base import MusifyItem -from musify.shared.core.collection import MusifyCollection -from musify.shared.core.misc import PrettyPrinter -from musify.shared.core.object import BasicCollection, Library, Playlist -from musify.shared.exception import MusifyTypeError -from musify.shared.remote.library import RemoteLibrary -from musify.shared.remote.object import RemoteCollectionLoader -from tests.shared.core.misc import PrettyPrinterTester +from tests.core.printer import PrettyPrinterTester -class ItemCollectionTester(PrettyPrinterTester, metaclass=ABCMeta): +class MusifyCollectionTester(PrettyPrinterTester, metaclass=ABCMeta): """ - Run generic tests for :py:class:`ItemCollection` implementations. + Run generic tests for :py:class:`MusifyCollection` implementations. The collection must have 3 or more items and all items must be unique. You must also provide a set of ``merge_items`` of the same items with different properties to merge with the collection. @@ -27,17 +28,23 @@ class ItemCollectionTester(PrettyPrinterTester, metaclass=ABCMeta): @abstractmethod def collection(self, *args, **kwargs) -> MusifyCollection: - """Yields an :py:class:`ItemCollection` object to be tested as pytest.fixture""" + """Yields an :py:class:`MusifyCollection` object to be tested as pytest.fixture""" raise NotImplementedError @abstractmethod def collection_merge_items(self, *args, **kwargs) -> Iterable[MusifyItem]: - """Yields an Iterable of :py:class:`Item` for use in :py:class:`ItemCollection` tests as pytest.fixture""" + """ + Yields an Iterable of :py:class:`MusifyItem` for use in :py:class:`MusifyCollection` tests + as pytest.fixture + """ raise NotImplementedError @abstractmethod def collection_merge_invalid(self, *args, **kwargs) -> Iterable[MusifyItem]: - """Yields an Iterable of :py:class:`Item` for use in :py:class:`ItemCollection` tests as pytest.fixture""" + """ + Yields an Iterable of :py:class:`MusifyItem` for use in :py:class:`MusifyCollection` tests + as pytest.fixture + """ raise NotImplementedError @pytest.fixture @@ -55,7 +62,7 @@ def test_collection_input_validation(collection: MusifyCollection, collection_me with pytest.raises(MusifyTypeError): collection.insert(0, next(c for c in collection_merge_invalid)) - if not isinstance(collection, RemoteLibrary): # overriden by remote libraries + if not isinstance(collection, RemoteLibrary): # overridden by remote libraries with pytest.raises(MusifyTypeError): collection.extend(collection_merge_invalid) with pytest.raises(MusifyTypeError): @@ -84,7 +91,7 @@ def test_collection_mutable_sequence_methods(collection: MusifyCollection): collection.append(item, allow_duplicates=False) assert len(collection) == length_start + 1 - if not isinstance(collection, RemoteLibrary): # overriden by remote libraries + if not isinstance(collection, RemoteLibrary): # overridden by remote libraries collection.extend(collection) assert len(collection) == (length_start + 1) * 2 assert collection.count(item) == 4 @@ -110,7 +117,7 @@ def test_collection_mutable_sequence_methods(collection: MusifyCollection): @staticmethod def test_collection_basic_dunder_methods(collection: MusifyCollection): - """:py:class:`ItemCollection` basic dunder operation tests""" + """:py:class:`MusifyCollection` basic dunder operation tests""" collection_original = deepcopy(collection) collection_basic = BasicCollection(name="this is a basic collection", items=collection.items) @@ -139,7 +146,7 @@ def test_collection_basic_dunder_methods(collection: MusifyCollection): def test_collection_iterator_and_container_dunder_methods( collection: MusifyCollection, collection_merge_items: Iterable[MusifyItem] ): - """:py:class:`ItemCollection` dunder iterator and contains tests""" + """:py:class:`MusifyCollection` dunder iterator and contains tests""" assert len([item for item in collection]) == len(collection.items) assert len([item for item in reversed(collection.items)]) == len(collection.items) for i, item in enumerate(reversed(collection.items)): @@ -192,7 +199,7 @@ def test_collection_sort(collection: MusifyCollection): assert collection == items -class PlaylistTester(ItemCollectionTester, metaclass=ABCMeta): +class PlaylistTester(MusifyCollectionTester, metaclass=ABCMeta): @abstractmethod def playlist(self, *args, **kwargs) -> Playlist: @@ -212,7 +219,7 @@ def test_merge_dunder_methods(self, playlist: Playlist): pass -class LibraryTester(ItemCollectionTester, metaclass=ABCMeta): +class LibraryTester(MusifyCollectionTester, metaclass=ABCMeta): """ Run generic tests for :py:class:`Library` implementations. The collection must have 3 or more playlists and all playlists must be unique. diff --git a/tests/shared/__init__.py b/tests/libraries/local/__init__.py similarity index 100% rename from tests/shared/__init__.py rename to tests/libraries/local/__init__.py diff --git a/tests/local/conftest.py b/tests/libraries/local/conftest.py similarity index 79% rename from tests/local/conftest.py rename to tests/libraries/local/conftest.py index d5eda642..7b435b39 100644 --- a/tests/local/conftest.py +++ b/tests/libraries/local/conftest.py @@ -1,10 +1,10 @@ import pytest -from musify.local.track import LocalTrack, FLAC, MP3, M4A, WMA -from musify.shared.file import PathStemMapper -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.spotify.processors import SpotifyDataWrangler -from tests.local.utils import path_track_all, path_track_flac, path_track_mp3, path_track_m4a, path_track_wma +from musify.file.path_mapper import PathStemMapper +from musify.libraries.local.track import LocalTrack, FLAC, MP3, M4A, WMA +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from tests.libraries.local.utils import path_track_all, path_track_flac, path_track_mp3, path_track_m4a, path_track_wma from tests.utils import path_resources diff --git a/tests/shared/api/__init__.py b/tests/libraries/local/library/__init__.py similarity index 100% rename from tests/shared/api/__init__.py rename to tests/libraries/local/library/__init__.py diff --git a/tests/local/library/test_library.py b/tests/libraries/local/library/test_library.py similarity index 86% rename from tests/local/library/test_library.py rename to tests/libraries/local/library/test_library.py index edd03427..352b6a2b 100644 --- a/tests/local/library/test_library.py +++ b/tests/libraries/local/library/test_library.py @@ -4,14 +4,15 @@ import pytest -from musify.local.library import LocalLibrary -from musify.local.track import LocalTrack +from musify.file.path_mapper import PathMapper, PathStemMapper +from musify.libraries.local.library import LocalLibrary +from musify.libraries.local.track import LocalTrack from musify.processors.filter import FilterDefinedList, FilterIncludeExclude -from musify.shared.file import PathMapper, PathStemMapper -from tests.local.library.testers import LocalLibraryTester -from tests.local.track.utils import random_track, random_tracks -from tests.local.utils import path_playlist_resources, path_playlist_all, path_playlist_m3u, path_playlist_xautopf_bp -from tests.local.utils import path_track_resources, path_track_all +from tests.libraries.local.library.testers import LocalLibraryTester +from tests.libraries.local.track.utils import random_track, random_tracks +from tests.libraries.local.utils import path_playlist_m3u, path_playlist_xautopf_bp +from tests.libraries.local.utils import path_playlist_resources, path_playlist_all +from tests.libraries.local.utils import path_track_resources, path_track_all from tests.utils import path_resources diff --git a/tests/local/library/test_musicbee.py b/tests/libraries/local/library/test_musicbee.py similarity index 89% rename from tests/local/library/test_musicbee.py rename to tests/libraries/local/library/test_musicbee.py index 2dafcfec..77edabe4 100644 --- a/tests/local/library/test_musicbee.py +++ b/tests/libraries/local/library/test_musicbee.py @@ -6,18 +6,20 @@ import pytest -from musify.local.library import LocalLibrary, MusicBee -from musify.local.library.musicbee import XMLLibraryParser -from musify.local.track import LocalTrack +from musify.file.exception import FileDoesNotExistError +from musify.file.path_mapper import PathMapper +from musify.libraries.local.library import LocalLibrary, MusicBee +from musify.libraries.local.library.musicbee import XMLLibraryParser +from musify.libraries.local.track import LocalTrack +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler from musify.processors.filter import FilterIncludeExclude, FilterDefinedList -from musify.shared.exception import FileDoesNotExistError -from musify.shared.file import PathMapper -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from tests.local.library.testers import LocalLibraryTester -from tests.local.track.utils import random_track -from tests.local.utils import path_library_resources -from tests.local.utils import path_playlist_resources, path_playlist_all, path_playlist_m3u, path_playlist_xautopf_bp -from tests.local.utils import path_track_all, path_track_mp3, path_track_flac, path_track_wma, path_track_resources +from tests.libraries.local.library.testers import LocalLibraryTester +from tests.libraries.local.track.utils import random_track +from tests.libraries.local.utils import path_library_resources +from tests.libraries.local.utils import path_playlist_m3u, path_playlist_xautopf_bp +from tests.libraries.local.utils import path_playlist_resources, path_playlist_all +from tests.libraries.local.utils import path_track_all, path_track_resources +from tests.libraries.local.utils import path_track_mp3, path_track_flac, path_track_wma from tests.utils import path_resources library_xml_filename = "musicbee_library.xml" diff --git a/tests/local/library/testers.py b/tests/libraries/local/library/testers.py similarity index 55% rename from tests/local/library/testers.py rename to tests/libraries/local/library/testers.py index ff02c48a..4a45aa30 100644 --- a/tests/local/library/testers.py +++ b/tests/libraries/local/library/testers.py @@ -2,9 +2,9 @@ import pytest -from musify.local.library import LocalLibrary -from tests.local.track.testers import LocalCollectionTester -from tests.shared.core.collection import LibraryTester +from musify.libraries.local.library import LocalLibrary +from tests.libraries.core.collection import LibraryTester +from tests.libraries.local.track.testers import LocalCollectionTester class LocalLibraryTester(LibraryTester, LocalCollectionTester, metaclass=ABCMeta): diff --git a/tests/shared/core/__init__.py b/tests/libraries/local/playlist/__init__.py similarity index 100% rename from tests/shared/core/__init__.py rename to tests/libraries/local/playlist/__init__.py diff --git a/tests/local/playlist/conftest.py b/tests/libraries/local/playlist/conftest.py similarity index 57% rename from tests/local/playlist/conftest.py rename to tests/libraries/local/playlist/conftest.py index 7a0a0135..66278d61 100644 --- a/tests/local/playlist/conftest.py +++ b/tests/libraries/local/playlist/conftest.py @@ -1,7 +1,7 @@ import pytest -from musify.local.track import LocalTrack, FLAC, M4A, MP3, WMA -from tests.local.utils import path_track_flac, path_track_m4a, path_track_wma, path_track_mp3 +from musify.libraries.local.track import LocalTrack, FLAC, M4A, MP3, WMA +from tests.libraries.local.utils import path_track_flac, path_track_m4a, path_track_wma, path_track_mp3 @pytest.fixture(scope="module") diff --git a/tests/local/playlist/test_m3u.py b/tests/libraries/local/playlist/test_m3u.py similarity index 93% rename from tests/local/playlist/test_m3u.py rename to tests/libraries/local/playlist/test_m3u.py index 1c3edc0f..56777a17 100644 --- a/tests/local/playlist/test_m3u.py +++ b/tests/libraries/local/playlist/test_m3u.py @@ -5,13 +5,13 @@ import pytest -from musify.local.playlist import M3U -from musify.local.track import LocalTrack -from musify.shared.exception import InvalidFileType -from musify.shared.file import PathMapper, PathStemMapper -from tests.local.playlist.testers import LocalPlaylistTester -from tests.local.track.utils import random_track, random_tracks -from tests.local.utils import path_playlist_m3u +from musify.file.exception import InvalidFileType +from musify.file.path_mapper import PathMapper, PathStemMapper +from musify.libraries.local.playlist import M3U +from musify.libraries.local.track import LocalTrack +from tests.libraries.local.playlist.testers import LocalPlaylistTester +from tests.libraries.local.track.utils import random_track, random_tracks +from tests.libraries.local.utils import path_playlist_m3u from tests.utils import path_txt diff --git a/tests/local/playlist/test_xautopf.py b/tests/libraries/local/playlist/test_xautopf.py similarity index 90% rename from tests/local/playlist/test_xautopf.py rename to tests/libraries/local/playlist/test_xautopf.py index 8c52cd3e..9adc778d 100644 --- a/tests/local/playlist/test_xautopf.py +++ b/tests/libraries/local/playlist/test_xautopf.py @@ -8,15 +8,15 @@ import pytest -from musify.local.library import MusicBee -from musify.local.playlist import XAutoPF, load_playlist -from musify.local.track import LocalTrack -from musify.shared.exception import InvalidFileType -from musify.shared.file import PathMapper, PathStemMapper -from tests.local.playlist.testers import LocalPlaylistTester -from tests.local.track.utils import random_track, random_tracks -from tests.local.utils import path_playlist_xautopf_ra, path_playlist_xautopf_bp -from tests.local.utils import path_track_flac, path_track_wma +from musify.file.exception import InvalidFileType +from musify.file.path_mapper import PathMapper, PathStemMapper +from musify.libraries.local.library import MusicBee +from musify.libraries.local.playlist import XAutoPF, load_playlist +from musify.libraries.local.track import LocalTrack +from tests.libraries.local.playlist.testers import LocalPlaylistTester +from tests.libraries.local.track.utils import random_track, random_tracks +from tests.libraries.local.utils import path_playlist_xautopf_ra, path_playlist_xautopf_bp +from tests.libraries.local.utils import path_track_flac, path_track_wma from tests.utils import path_txt @@ -171,7 +171,7 @@ def test_save_playlist(self, tracks: list[LocalTrack], path: str, path_mapper: P assert path.startswith("../") -# noinspection PyTestUnpassedFixture +# noinspection PyTestUnpassedFixture, SpellCheckingInspection @pytest.mark.manual @pytest.mark.skipif( "not config.getoption('-m') and not config.getoption('-k')", diff --git a/tests/libraries/local/playlist/testers.py b/tests/libraries/local/playlist/testers.py new file mode 100644 index 00000000..1ba39010 --- /dev/null +++ b/tests/libraries/local/playlist/testers.py @@ -0,0 +1,8 @@ +from abc import ABCMeta + +from tests.libraries.core.collection import PlaylistTester +from tests.libraries.local.track.testers import LocalCollectionTester + + +class LocalPlaylistTester(PlaylistTester, LocalCollectionTester, metaclass=ABCMeta): + pass diff --git a/tests/local/test_collection.py b/tests/libraries/local/test_collection.py similarity index 94% rename from tests/local/test_collection.py rename to tests/libraries/local/test_collection.py index b72296b6..82d17e8f 100644 --- a/tests/local/test_collection.py +++ b/tests/libraries/local/test_collection.py @@ -2,12 +2,12 @@ import pytest -from musify.local.collection import LocalFolder, LocalAlbum, LocalArtist, LocalGenres -from musify.local.exception import LocalCollectionError -from musify.local.track import LocalTrack -from tests.local.track.testers import LocalCollectionTester -from tests.local.track.utils import random_track, random_tracks -from tests.local.utils import path_track_resources, path_track_all +from musify.libraries.local.collection import LocalFolder, LocalAlbum, LocalArtist, LocalGenres +from musify.libraries.local.exception import LocalCollectionError +from musify.libraries.local.track import LocalTrack +from tests.libraries.local.track.testers import LocalCollectionTester +from tests.libraries.local.track.utils import random_track, random_tracks +from tests.libraries.local.utils import path_track_resources, path_track_all from tests.utils import random_str diff --git a/tests/local/test_file.py b/tests/libraries/local/test_file.py similarity index 91% rename from tests/local/test_file.py rename to tests/libraries/local/test_file.py index 97d97c87..c32ea468 100644 --- a/tests/local/test_file.py +++ b/tests/libraries/local/test_file.py @@ -2,10 +2,11 @@ import pytest -from musify.shared.file import PathMapper, PathStemMapper, File -from tests.local.track.utils import random_tracks -from tests.local.utils import path_track_all -from tests.shared.core.misc import PrettyPrinterTester +from musify.file.base import File +from musify.file.path_mapper import PathMapper, PathStemMapper +from tests.core.printer import PrettyPrinterTester +from tests.libraries.local.track.utils import random_tracks +from tests.libraries.local.utils import path_track_all from tests.utils import random_str diff --git a/tests/shared/remote/__init__.py b/tests/libraries/local/track/__init__.py similarity index 100% rename from tests/shared/remote/__init__.py rename to tests/libraries/local/track/__init__.py diff --git a/tests/local/track/test_track.py b/tests/libraries/local/track/test_track.py similarity index 94% rename from tests/local/track/test_track.py rename to tests/libraries/local/track/test_track.py index efbb0f0f..e4a35808 100644 --- a/tests/local/track/test_track.py +++ b/tests/libraries/local/track/test_track.py @@ -6,17 +6,17 @@ import pytest from PIL.Image import Image -from musify.local.track import LocalTrack, load_track, FLAC, M4A, MP3, WMA, SyncResultTrack -from musify.local.track.field import LocalTrackField -from musify.shared.core.base import MusifyItem -from musify.shared.core.object import Track -from musify.shared.exception import InvalidFileType, FileDoesNotExistError -from musify.shared.exception import MusifyKeyError -from musify.shared.image import open_image -from musify.shared.remote.enum import RemoteObjectType -from tests.local.utils import path_track_all, path_track_img, path_track_resources -from tests.shared.core.base import ItemTester -from tests.spotify.utils import random_uri +from musify.core.base import MusifyItem +from musify.exception import MusifyKeyError +from musify.file.exception import InvalidFileType, FileDoesNotExistError +from musify.file.image import open_image +from musify.libraries.core.object import Track +from musify.libraries.local.track import LocalTrack, load_track, FLAC, M4A, MP3, WMA, SyncResultTrack +from musify.libraries.local.track.field import LocalTrackField +from musify.libraries.remote.core.enum import RemoteObjectType +from tests.core.base import MusifyItemTester +from tests.libraries.local.utils import path_track_all, path_track_img, path_track_resources +from tests.libraries.remote.spotify.utils import random_uri from tests.utils import path_txt @@ -193,7 +193,7 @@ def test_loaded_attributes_common(track: LocalTrack): assert track.play_count is None -class TestLocalTrack(ItemTester): +class TestLocalTrack(MusifyItemTester): """Run generic tests for :py:class:`LocalTrack` implementations""" @pytest.fixture diff --git a/tests/local/track/testers.py b/tests/libraries/local/track/testers.py similarity index 76% rename from tests/local/track/testers.py rename to tests/libraries/local/track/testers.py index 5666ed9f..a25d7dcb 100644 --- a/tests/local/track/testers.py +++ b/tests/libraries/local/track/testers.py @@ -4,16 +4,16 @@ import pytest -from musify.local.collection import LocalCollection -from musify.local.track import LocalTrack -from musify.shared.exception import MusifyKeyError -from musify.spotify.object import SpotifyTrack -from tests.local.track.utils import random_tracks -from tests.shared.core.collection import ItemCollectionTester -from tests.spotify.api.mock import SpotifyMock +from musify.exception import MusifyKeyError +from musify.libraries.local.collection import LocalCollection +from musify.libraries.local.track import LocalTrack +from musify.libraries.remote.spotify.object import SpotifyTrack +from tests.libraries.core.collection import MusifyCollectionTester +from tests.libraries.local.track.utils import random_tracks +from tests.libraries.remote.spotify.api.mock import SpotifyMock -class LocalCollectionTester(ItemCollectionTester, metaclass=ABCMeta): +class LocalCollectionTester(MusifyCollectionTester, metaclass=ABCMeta): @pytest.fixture def collection_merge_items(self) -> Iterable[LocalTrack]: @@ -26,7 +26,7 @@ def collection_merge_invalid(self, spotify_mock: SpotifyMock) -> Collection[Spot def test_collection_getitem_dunder_method( self, collection: LocalCollection, collection_merge_items: Iterable[LocalTrack] ): - """:py:class:`ItemCollection` __getitem__ and __setitem__ tests""" + """:py:class:`MusifyCollection` __getitem__ and __setitem__ tests""" idx, item = next((i, item) for i, item in enumerate(collection.items) if collection.items.count(item) == 1) assert collection[1] == collection.items[1] diff --git a/tests/local/track/utils.py b/tests/libraries/local/track/utils.py similarity index 91% rename from tests/local/track/utils.py rename to tests/libraries/local/track/utils.py index 9cf23729..4d36f217 100644 --- a/tests/local/track/utils.py +++ b/tests/libraries/local/track/utils.py @@ -6,9 +6,9 @@ import mutagen from dateutil.relativedelta import relativedelta -from musify.local.track import TRACK_CLASSES, LocalTrack -from tests.local.utils import remote_wrangler, path_track_resources -from tests.spotify.utils import random_uri +from musify.libraries.local.track import TRACK_CLASSES, LocalTrack +from tests.libraries.local.utils import remote_wrangler, path_track_resources +from tests.libraries.remote.spotify.utils import random_uri from tests.utils import random_str, random_dt, random_genres diff --git a/tests/local/utils.py b/tests/libraries/local/utils.py similarity index 84% rename from tests/local/utils.py rename to tests/libraries/local/utils.py index 8ef450ea..7299f08c 100644 --- a/tests/local/utils.py +++ b/tests/libraries/local/utils.py @@ -1,8 +1,8 @@ from os.path import join -from musify.local.playlist import PLAYLIST_CLASSES -from musify.local.track import TRACK_CLASSES -from musify.spotify.processors import SpotifyDataWrangler +from musify.libraries.local.playlist import PLAYLIST_CLASSES +from musify.libraries.local.track import TRACK_CLASSES +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler from tests.utils import path_resources path_track_resources = join(path_resources, "track") diff --git a/tests/shared/remote/processors/__init__.py b/tests/libraries/remote/__init__.py similarity index 100% rename from tests/shared/remote/processors/__init__.py rename to tests/libraries/remote/__init__.py diff --git a/tests/spotify/__init__.py b/tests/libraries/remote/core/__init__.py similarity index 100% rename from tests/spotify/__init__.py rename to tests/libraries/remote/core/__init__.py diff --git a/tests/shared/remote/api.py b/tests/libraries/remote/core/api.py similarity index 92% rename from tests/shared/remote/api.py rename to tests/libraries/remote/core/api.py index d39b7e44..c4d32dfc 100644 --- a/tests/shared/remote/api.py +++ b/tests/libraries/remote/core/api.py @@ -8,10 +8,10 @@ # noinspection PyProtectedMember,PyUnresolvedReferences from requests_mock.request import _RequestObjectProxy as Request -from musify.shared.remote.api import RemoteAPI -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.factory import RemoteObjectFactory -from tests.shared.remote.utils import RemoteMock +from musify.libraries.remote.core.api import RemoteAPI +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.factory import RemoteObjectFactory +from tests.libraries.remote.core.utils import RemoteMock class RemoteAPITester(ABC): diff --git a/tests/shared/remote/library.py b/tests/libraries/remote/core/library.py similarity index 93% rename from tests/shared/remote/library.py rename to tests/libraries/remote/core/library.py index a40f2a27..5d4dc003 100644 --- a/tests/shared/remote/library.py +++ b/tests/libraries/remote/core/library.py @@ -4,14 +4,14 @@ from random import choice from typing import Any -from musify.shared.core.base import MusifyItem -from musify.shared.core.object import Playlist -from musify.shared.remote.library import RemoteLibrary -from musify.shared.remote.object import RemoteTrack -from tests.local.track.utils import random_tracks -from tests.shared.core.collection import LibraryTester -from tests.shared.remote.object import RemoteCollectionTester -from tests.shared.remote.utils import RemoteMock +from musify.core.base import MusifyItem +from musify.libraries.core.object import Playlist +from musify.libraries.remote.core.library import RemoteLibrary +from musify.libraries.remote.core.object import RemoteTrack +from tests.libraries.core.collection import LibraryTester +from tests.libraries.local.track.utils import random_tracks +from tests.libraries.remote.core.object import RemoteCollectionTester +from tests.libraries.remote.core.utils import RemoteMock class RemoteLibraryTester(RemoteCollectionTester, LibraryTester, metaclass=ABCMeta): @@ -19,7 +19,7 @@ class RemoteLibraryTester(RemoteCollectionTester, LibraryTester, metaclass=ABCMe @abstractmethod def collection_merge_items(self, *args, **kwargs) -> list[RemoteTrack]: """ - Yields an Iterable of :py:class:`RemoteTrack` for use in :py:class:`ItemCollection` tests as pytest.fixture. + Yields an Iterable of :py:class:`RemoteTrack` for use in :py:class:`MusifyCollection` tests as pytest.fixture. This must be a valid set of tracks that can be successfully called by the api_mock fixture. """ raise NotImplementedError @@ -239,7 +239,7 @@ def test_sync(self, library: RemoteLibrary, collection_merge_items: list[RemoteT for track in collection_merge_items: assert track not in pl_actual - # Mapping[str, Iterable[Item]] + # Mapping[str, Iterable[MusifyItem]] playlists_tracks = { name_actual: pl_actual[:5] + collection_merge_items, name_new: collection_merge_items, diff --git a/tests/shared/remote/object.py b/tests/libraries/remote/core/object.py similarity index 91% rename from tests/shared/remote/object.py rename to tests/libraries/remote/core/object.py index 6440de45..4db449f3 100644 --- a/tests/shared/remote/object.py +++ b/tests/libraries/remote/core/object.py @@ -4,17 +4,17 @@ import pytest -from musify.local.track import LocalTrack -from musify.shared.exception import MusifyKeyError -from musify.shared.remote.api import RemoteAPI -from musify.shared.remote.base import RemoteItem -from musify.shared.remote.object import RemoteTrack, RemoteCollection, RemotePlaylist -from tests.local.track.utils import random_tracks -from tests.shared.core.collection import ItemCollectionTester, PlaylistTester -from tests.shared.remote.utils import RemoteMock +from musify.exception import MusifyKeyError +from musify.libraries.local.track import LocalTrack +from musify.libraries.remote.core.api import RemoteAPI +from musify.libraries.remote.core.base import RemoteItem +from musify.libraries.remote.core.object import RemoteTrack, RemoteCollection, RemotePlaylist +from tests.libraries.core.collection import MusifyCollectionTester, PlaylistTester +from tests.libraries.local.track.utils import random_tracks +from tests.libraries.remote.core.utils import RemoteMock -class RemoteCollectionTester(ItemCollectionTester, metaclass=ABCMeta): +class RemoteCollectionTester(MusifyCollectionTester, metaclass=ABCMeta): @abstractmethod def collection_merge_items(self, *args, **kwargs) -> Iterable[RemoteItem]: @@ -27,7 +27,7 @@ def collection_merge_invalid(self, *_, **__) -> Iterable[LocalTrack]: def test_collection_getitem_dunder_method( self, collection: RemoteCollection, collection_merge_items: Iterable[RemoteItem] ): - """:py:class:`ItemCollection` __getitem__ and __setitem__ tests""" + """:py:class:`MusifyCollection` __getitem__ and __setitem__ tests""" idx, item = next((i, item) for i, item in enumerate(collection.items) if collection.items.count(item) == 1) assert collection[1] == collection.items[1] diff --git a/tests/spotify/api/__init__.py b/tests/libraries/remote/core/processors/__init__.py similarity index 100% rename from tests/spotify/api/__init__.py rename to tests/libraries/remote/core/processors/__init__.py diff --git a/tests/shared/remote/processors/check.py b/tests/libraries/remote/core/processors/check.py similarity index 94% rename from tests/shared/remote/processors/check.py rename to tests/libraries/remote/core/processors/check.py index abfaa1df..0f8db42a 100644 --- a/tests/shared/remote/processors/check.py +++ b/tests/libraries/remote/core/processors/check.py @@ -6,17 +6,17 @@ import pytest from pytest_mock import MockerFixture -from musify.local.track import LocalTrack -from musify.shared.core.object import BasicCollection -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.object import RemotePlaylist -from musify.shared.remote.processors.check import RemoteItemChecker -from tests.local.track.utils import random_track, random_tracks -from tests.shared.api.utils import path_token -from tests.shared.core.misc import PrettyPrinterTester -from tests.shared.remote.processors.utils import patch_input -from tests.shared.remote.utils import RemoteMock -from tests.spotify.utils import random_uri, random_uris +from musify.libraries.collection import BasicCollection +from musify.libraries.local.track import LocalTrack +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.object import RemotePlaylist +from musify.libraries.remote.core.processors.check import RemoteItemChecker +from tests.api.utils import path_token +from tests.core.printer import PrettyPrinterTester +from tests.libraries.local.track.utils import random_track, random_tracks +from tests.libraries.remote.core.processors.utils import patch_input +from tests.libraries.remote.core.utils import RemoteMock +from tests.libraries.remote.spotify.utils import random_uri, random_uris from tests.utils import random_str, get_stdout @@ -50,7 +50,6 @@ def setup_playlist_collection( ) -> tuple[RemotePlaylist, BasicCollection]: """Setups up checker, playlist, and collection for testing match_to_remote functionality""" url = choice(playlist_urls) - # noinspection PyProtectedMember pl = checker.factory.playlist(checker.api.get_items(url, extend=True, use_cache=False)[0]) assert len(pl) > 10 assert len({item.uri for item in pl}) == len(pl) # all unique tracks diff --git a/tests/shared/remote/processors/search.py b/tests/libraries/remote/core/processors/search.py similarity index 91% rename from tests/shared/remote/processors/search.py rename to tests/libraries/remote/core/processors/search.py index b030b57e..603b1b12 100644 --- a/tests/shared/remote/processors/search.py +++ b/tests/libraries/remote/core/processors/search.py @@ -5,17 +5,18 @@ import pytest -from musify.local.collection import LocalAlbum -from musify.local.track import LocalTrack -from musify.shared.core.base import MusifyItem -from musify.shared.core.collection import MusifyCollection -from musify.shared.core.enum import TagFields as Tag -from musify.shared.core.object import BasicCollection, Album -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.processors.search import RemoteItemSearcher, SearchSettings -from tests.local.track.utils import random_track, random_tracks -from tests.shared.core.misc import PrettyPrinterTester -from tests.shared.remote.utils import RemoteMock +from musify.core.base import MusifyItem +from musify.core.enum import TagFields as Tag +from musify.libraries.collection import BasicCollection +from musify.libraries.core.collection import MusifyCollection +from musify.libraries.core.object import Album +from musify.libraries.local.collection import LocalAlbum +from musify.libraries.local.track import LocalTrack +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.processors.search import RemoteItemSearcher, SearchSettings +from tests.core.printer import PrettyPrinterTester +from tests.libraries.local.track.utils import random_track, random_tracks +from tests.libraries.remote.core.utils import RemoteMock class RemoteItemSearcherTester(PrettyPrinterTester, metaclass=ABCMeta): @@ -48,7 +49,6 @@ def search_albums(self, *args, **kwargs) -> list[LocalAlbum]: """ raise NotImplementedError - # noinspection PyProtectedMember @pytest.fixture def unmatchable_items(self) -> list[LocalTrack]: """ @@ -61,6 +61,7 @@ def unmatchable_items(self) -> list[LocalTrack]: assert item.has_uri is None item.title = item.artist = item.album = None + # noinspection PyProtectedMember item._reader.file.info.length = -3000 item.year = 1000 diff --git a/tests/shared/remote/processors/utils.py b/tests/libraries/remote/core/processors/utils.py similarity index 100% rename from tests/shared/remote/processors/utils.py rename to tests/libraries/remote/core/processors/utils.py diff --git a/tests/shared/remote/utils.py b/tests/libraries/remote/core/utils.py similarity index 95% rename from tests/shared/remote/utils.py rename to tests/libraries/remote/core/utils.py index b1e72981..f3411d84 100644 --- a/tests/shared/remote/utils.py +++ b/tests/libraries/remote/core/utils.py @@ -8,7 +8,7 @@ # noinspection PyProtectedMember,PyUnresolvedReferences from requests_mock.request import _RequestObjectProxy -from musify.shared.remote.enum import RemoteIDType, RemoteObjectType +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType ALL_ID_TYPES = RemoteIDType.all() ALL_ITEM_TYPES = RemoteObjectType.all() diff --git a/tests/spotify/object/__init__.py b/tests/libraries/remote/spotify/__init__.py similarity index 100% rename from tests/spotify/object/__init__.py rename to tests/libraries/remote/spotify/__init__.py diff --git a/tests/libraries/remote/spotify/api/__init__.py b/tests/libraries/remote/spotify/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/spotify/api/mock.py b/tests/libraries/remote/spotify/api/mock.py similarity index 97% rename from tests/spotify/api/mock.py rename to tests/libraries/remote/spotify/api/mock.py index d4c44fb0..d0bd50f6 100644 --- a/tests/spotify/api/mock.py +++ b/tests/libraries/remote/spotify/api/mock.py @@ -13,11 +13,11 @@ # noinspection PyProtectedMember,PyUnresolvedReferences from requests_mock.response import _Context as Context -from musify.shared.remote.enum import RemoteObjectType as ObjectType -from musify.spotify.api import SpotifyAPI -from musify.spotify.processors import SpotifyDataWrangler -from tests.shared.remote.utils import RemoteMock -from tests.spotify.utils import random_id +from musify.libraries.remote.core.enum import RemoteObjectType as ObjectType +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from tests.libraries.remote.core.utils import RemoteMock +from tests.libraries.remote.spotify.utils import random_id from tests.utils import random_str, random_date_str, random_dt, random_genres # noinspection PyUnresolvedReferences diff --git a/tests/spotify/api/test_artist.py b/tests/libraries/remote/spotify/api/test_artist.py similarity index 92% rename from tests/spotify/api/test_artist.py rename to tests/libraries/remote/spotify/api/test_artist.py index c561f655..32dc8c01 100644 --- a/tests/spotify/api/test_artist.py +++ b/tests/libraries/remote/spotify/api/test_artist.py @@ -5,13 +5,13 @@ import pytest -from musify.shared.remote.enum import RemoteObjectType -from musify.spotify.api import SpotifyAPI -from musify.spotify.api.item import ARTIST_ALBUM_TYPES -from musify.spotify.object import SpotifyArtist -from tests.spotify.api.mock import SpotifyMock -from tests.spotify.api.utils import assert_calls, get_limit -from tests.spotify.utils import random_id_type, random_id_types +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.api.item import ARTIST_ALBUM_TYPES +from musify.libraries.remote.spotify.object import SpotifyArtist +from tests.libraries.remote.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.api.utils import assert_calls, get_limit +from tests.libraries.remote.spotify.utils import random_id_type, random_id_types class TestSpotifyAPIArtists: diff --git a/tests/spotify/api/test_item.py b/tests/libraries/remote/spotify/api/test_item.py similarity index 94% rename from tests/spotify/api/test_item.py rename to tests/libraries/remote/spotify/api/test_item.py index e34d8894..94171d44 100644 --- a/tests/spotify/api/test_item.py +++ b/tests/libraries/remote/spotify/api/test_item.py @@ -7,19 +7,19 @@ import pytest -from musify.shared.api.exception import APIError -from musify.shared.remote import RemoteResponse -from musify.shared.remote.enum import RemoteObjectType, RemoteIDType as IDType, RemoteIDType -from musify.shared.remote.exception import RemoteObjectTypeError -from musify.shared.remote.object import RemoteCollection -from musify.spotify.api import SpotifyAPI -from musify.spotify.factory import SpotifyObjectFactory -from musify.spotify.object import SpotifyPlaylist, SpotifyAlbum, SpotifyTrack -from tests.shared.remote.api import RemoteAPITester -from tests.shared.remote.utils import ALL_ITEM_TYPES -from tests.spotify.api.mock import SpotifyMock -from tests.spotify.api.utils import get_limit, assert_calls -from tests.spotify.utils import random_ids, random_id, random_id_type, random_id_types +from musify.api.exception import APIError +from musify.libraries.remote.core import RemoteResponse +from musify.libraries.remote.core.enum import RemoteObjectType, RemoteIDType +from musify.libraries.remote.core.exception import RemoteObjectTypeError +from musify.libraries.remote.core.object import RemoteCollection +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.factory import SpotifyObjectFactory +from musify.libraries.remote.spotify.object import SpotifyPlaylist, SpotifyAlbum, SpotifyTrack +from tests.libraries.remote.core.api import RemoteAPITester +from tests.libraries.remote.core.utils import ALL_ITEM_TYPES +from tests.libraries.remote.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.api.utils import get_limit, assert_calls +from tests.libraries.remote.spotify.utils import random_ids, random_id, random_id_type, random_id_types from tests.utils import idfn, random_str @@ -33,7 +33,6 @@ def object_factory(self) -> SpotifyObjectFactory: """Yield the object factory for Spotify objects as a pytest.fixture""" return SpotifyObjectFactory() - # noinspection PyMethodOverriding @pytest.fixture def responses(self, _responses: dict[str, dict[str, Any]], key: str) -> dict[str, dict[str, Any]]: return {id_: response for id_, response in _responses.items() if key is None or response[key]["total"] > 3} @@ -228,14 +227,18 @@ def test_extend_tracks_input_validation(self, api: SpotifyAPI): assert api.extend_tracks(values=random_ids(), features=False, analysis=False) == [] assert api.extend_tracks(values=[], features=True, analysis=True) == [] - value = api.wrangler.convert(random_id(), kind=RemoteObjectType.ALBUM, type_in=IDType.ID, type_out=IDType.URL) + value = api.wrangler.convert( + random_id(), kind=RemoteObjectType.ALBUM, type_in=RemoteIDType.ID, type_out=RemoteIDType.URL + ) with pytest.raises(RemoteObjectTypeError): api.extend_tracks(values=value, features=True) def test_get_artist_albums_input_validation(self, api: SpotifyAPI): assert api.get_artist_albums(values=[]) == {} - value = api.wrangler.convert(random_id(), kind=RemoteObjectType.ALBUM, type_in=IDType.ID, type_out=IDType.URL) + value = api.wrangler.convert( + random_id(), kind=RemoteObjectType.ALBUM, type_in=RemoteIDType.ID, type_out=RemoteIDType.URL + ) with pytest.raises(RemoteObjectTypeError): api.get_artist_albums(values=value) @@ -801,7 +804,6 @@ def test_extend_tracks_single_response( assert test.bpm is not None # noinspection PyTestUnpassedFixture - @pytest.mark.parametrize("object_type", [RemoteObjectType.TRACK], ids=idfn) def test_extend_tracks_many_response( self, diff --git a/tests/spotify/api/test_misc.py b/tests/libraries/remote/spotify/api/test_misc.py similarity index 92% rename from tests/spotify/api/test_misc.py rename to tests/libraries/remote/spotify/api/test_misc.py index 1a19ff80..ecde3e9f 100644 --- a/tests/spotify/api/test_misc.py +++ b/tests/libraries/remote/spotify/api/test_misc.py @@ -4,10 +4,10 @@ import pytest -from musify.shared.remote.enum import RemoteObjectType as ObjectType -from musify.spotify.api import SpotifyAPI -from musify.spotify.processors import SpotifyDataWrangler -from tests.spotify.api.mock import SpotifyMock +from musify.libraries.remote.core.enum import RemoteObjectType as ObjectType +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from tests.libraries.remote.spotify.api.mock import SpotifyMock from tests.utils import idfn, get_stdout, random_str diff --git a/tests/spotify/api/test_playlist.py b/tests/libraries/remote/spotify/api/test_playlist.py similarity index 88% rename from tests/spotify/api/test_playlist.py rename to tests/libraries/remote/spotify/api/test_playlist.py index 11714890..e130443f 100644 --- a/tests/spotify/api/test_playlist.py +++ b/tests/libraries/remote/spotify/api/test_playlist.py @@ -5,14 +5,14 @@ import pytest from musify import PROGRAM_NAME -from musify.shared.remote.enum import RemoteObjectType as ObjectType, RemoteIDType -from musify.shared.remote.exception import RemoteObjectTypeError, RemoteIDTypeError -from musify.spotify.api import SpotifyAPI -from musify.spotify.object import SpotifyPlaylist -from tests.shared.remote.utils import ALL_ITEM_TYPES -from tests.spotify.api.mock import SpotifyMock -from tests.spotify.utils import random_ids, random_id, random_id_type, random_id_types -from tests.spotify.utils import random_uris, random_api_urls, random_ext_urls +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType +from musify.libraries.remote.core.exception import RemoteObjectTypeError, RemoteIDTypeError +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.object import SpotifyPlaylist +from tests.libraries.remote.core.utils import ALL_ITEM_TYPES +from tests.libraries.remote.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.utils import random_ids, random_id, random_id_type, random_id_types +from tests.libraries.remote.spotify.utils import random_uris, random_api_urls, random_ext_urls class TestSpotifyAPIPlaylists: @@ -64,7 +64,7 @@ def test_create_playlist(self, api: SpotifyAPI, api_mock: SpotifyMock): def test_add_to_playlist_input_validation_and_skips(self, api: SpotifyAPI, api_mock: SpotifyMock): url = f"{api.url}/playlists/{random_id()}" for kind in ALL_ITEM_TYPES: - if kind == ObjectType.TRACK: + if kind == RemoteObjectType.TRACK: continue with pytest.raises(RemoteObjectTypeError): @@ -106,7 +106,7 @@ def test_add_to_playlist(self, playlist: dict[str, Any], api: SpotifyAPI, api_mo assert total > limit # ensure ranges are valid for test to work id_list = random_id_types( - wrangler=api.wrangler, kind=ObjectType.TRACK, start=total - api_mock.limit_lower, stop=total - 1 + wrangler=api.wrangler, kind=RemoteObjectType.TRACK, start=total - api_mock.limit_lower, stop=total - 1 ) assert len(id_list) < total @@ -143,7 +143,7 @@ def test_add_to_playlist_with_skip(self, playlist: dict[str, Any], api: SpotifyA source = sample(playlist["tracks"]["items"], k=randrange(start=initial // 3, stop=initial // 2)) id_list_dupes = [item["track"]["id"] for item in source] id_list_new = random_id_types( - wrangler=api.wrangler, kind=ObjectType.TRACK, start=api_mock.limit_lower, stop=randrange(20, 30) + wrangler=api.wrangler, kind=RemoteObjectType.TRACK, start=api_mock.limit_lower, stop=randrange(20, 30) ) result = api.add_to_playlist(playlist=playlist["uri"], items=id_list_dupes + id_list_new, limit=limit) @@ -164,7 +164,7 @@ def test_add_to_playlist_with_skip(self, playlist: dict[str, Any], api: SpotifyA ########################################################################### def test_delete_playlist(self, playlist_unique: dict[str, Any], api: SpotifyAPI, api_mock: SpotifyMock): result = api.delete_playlist( - random_id_type(id_=playlist_unique["id"], wrangler=api.wrangler, kind=ObjectType.PLAYLIST) + random_id_type(id_=playlist_unique["id"], wrangler=api.wrangler, kind=RemoteObjectType.PLAYLIST) ) assert result == playlist_unique["href"] + "/followers" @@ -177,7 +177,7 @@ def test_delete_playlist(self, playlist_unique: dict[str, Any], api: SpotifyAPI, def test_clear_from_playlist_input_validation_and_skips(self, api: SpotifyAPI, api_mock: SpotifyMock): url = f"{api.url}/playlists/{random_id()}" for kind in ALL_ITEM_TYPES: - if kind == ObjectType.TRACK: + if kind == RemoteObjectType.TRACK: continue with pytest.raises(RemoteObjectTypeError): @@ -221,7 +221,7 @@ def test_clear_from_playlist_items(self, playlist: dict[str, Any], api: SpotifyA assert total > limit # ensure ranges are valid for test to work id_list = random_id_types( - wrangler=api.wrangler, kind=ObjectType.TRACK, start=total - api_mock.limit_lower, stop=total - 1 + wrangler=api.wrangler, kind=RemoteObjectType.TRACK, start=total - api_mock.limit_lower, stop=total - 1 ) assert len(id_list) < total diff --git a/tests/spotify/api/utils.py b/tests/libraries/remote/spotify/api/utils.py similarity index 93% rename from tests/spotify/api/utils.py rename to tests/libraries/remote/spotify/api/utils.py index 2df1258b..8aebfd8c 100644 --- a/tests/spotify/api/utils.py +++ b/tests/libraries/remote/spotify/api/utils.py @@ -5,7 +5,7 @@ # noinspection PyProtectedMember,PyUnresolvedReferences from requests_mock.request import _RequestObjectProxy as Request -from tests.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.api.mock import SpotifyMock def get_limit(values: Collection | int, max_limit: int, pages: int = 3) -> int: diff --git a/tests/spotify/conftest.py b/tests/libraries/remote/spotify/conftest.py similarity index 73% rename from tests/spotify/conftest.py rename to tests/libraries/remote/spotify/conftest.py index b88a3343..dc3a38d1 100644 --- a/tests/spotify/conftest.py +++ b/tests/libraries/remote/spotify/conftest.py @@ -1,8 +1,8 @@ import pytest -from musify.spotify.api import SpotifyAPI -from musify.spotify.processors import SpotifyDataWrangler -from tests.spotify.api.mock import SpotifyMock +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from tests.libraries.remote.spotify.api.mock import SpotifyMock @pytest.fixture(scope="module") diff --git a/tests/libraries/remote/spotify/object/__init__.py b/tests/libraries/remote/spotify/object/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/spotify/object/test_album.py b/tests/libraries/remote/spotify/object/test_album.py similarity index 93% rename from tests/spotify/object/test_album.py rename to tests/libraries/remote/spotify/object/test_album.py index 53488dc9..5cb4d6db 100644 --- a/tests/spotify/object/test_album.py +++ b/tests/libraries/remote/spotify/object/test_album.py @@ -7,15 +7,15 @@ import pytest -from musify.shared.api.exception import APIError -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.exception import RemoteObjectTypeError, RemoteError -from musify.shared.types import Number -from musify.spotify.api import SpotifyAPI -from musify.spotify.object import SpotifyAlbum, SpotifyTrack -from tests.spotify.api.mock import SpotifyMock -from tests.spotify.object.testers import SpotifyCollectionLoaderTester -from tests.spotify.utils import assert_id_attributes +from musify.api.exception import APIError +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.exception import RemoteObjectTypeError, RemoteError +from musify.types import Number +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.object import SpotifyAlbum, SpotifyTrack +from tests.libraries.remote.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.object.testers import SpotifyCollectionLoaderTester +from tests.libraries.remote.spotify.utils import assert_id_attributes class TestSpotifyAlbum(SpotifyCollectionLoaderTester): diff --git a/tests/spotify/object/test_artist.py b/tests/libraries/remote/spotify/object/test_artist.py similarity index 93% rename from tests/spotify/object/test_artist.py rename to tests/libraries/remote/spotify/object/test_artist.py index 7c77786f..d3b17936 100644 --- a/tests/spotify/object/test_artist.py +++ b/tests/libraries/remote/spotify/object/test_artist.py @@ -6,15 +6,15 @@ import pytest -from musify.shared.api.exception import APIError -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.exception import RemoteObjectTypeError -from musify.spotify.api import SpotifyAPI -from musify.spotify.object import SpotifyAlbum, SpotifyArtist -from musify.spotify.processors import SpotifyDataWrangler -from tests.spotify.object.testers import SpotifyCollectionLoaderTester -from tests.spotify.api.mock import SpotifyMock -from tests.spotify.utils import assert_id_attributes +from musify.api.exception import APIError +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.exception import RemoteObjectTypeError +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.object import SpotifyAlbum, SpotifyArtist +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from tests.libraries.remote.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.object.testers import SpotifyCollectionLoaderTester +from tests.libraries.remote.spotify.utils import assert_id_attributes class TestSpotifyArtist(SpotifyCollectionLoaderTester): diff --git a/tests/spotify/object/test_playlist.py b/tests/libraries/remote/spotify/object/test_playlist.py similarity index 93% rename from tests/spotify/object/test_playlist.py rename to tests/libraries/remote/spotify/object/test_playlist.py index 75d80e1b..53862fe9 100644 --- a/tests/spotify/object/test_playlist.py +++ b/tests/libraries/remote/spotify/object/test_playlist.py @@ -8,16 +8,16 @@ import pytest from musify import PROGRAM_NAME -from musify.shared.api.exception import APIError -from musify.shared.remote.enum import RemoteObjectType -from musify.shared.remote.exception import RemoteObjectTypeError, RemoteError -from musify.spotify.api import SpotifyAPI -from musify.spotify.exception import SpotifyCollectionError -from musify.spotify.object import SpotifyPlaylist, SpotifyTrack -from tests.shared.remote.object import RemotePlaylistTester -from tests.spotify.api.mock import SpotifyMock -from tests.spotify.object.testers import SpotifyCollectionLoaderTester -from tests.spotify.utils import random_uri, assert_id_attributes +from musify.api.exception import APIError +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.core.exception import RemoteObjectTypeError, RemoteError +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.exception import SpotifyCollectionError +from musify.libraries.remote.spotify.object import SpotifyPlaylist, SpotifyTrack +from tests.libraries.remote.core.object import RemotePlaylistTester +from tests.libraries.remote.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.object.testers import SpotifyCollectionLoaderTester +from tests.libraries.remote.spotify.utils import random_uri, assert_id_attributes class TestSpotifyPlaylist(SpotifyCollectionLoaderTester, RemotePlaylistTester): diff --git a/tests/spotify/object/test_track.py b/tests/libraries/remote/spotify/object/test_track.py similarity index 92% rename from tests/spotify/object/test_track.py rename to tests/libraries/remote/spotify/object/test_track.py index f8eafe29..2e0d2f35 100644 --- a/tests/spotify/object/test_track.py +++ b/tests/libraries/remote/spotify/object/test_track.py @@ -5,17 +5,17 @@ import pytest -from musify.shared.api.exception import APIError -from musify.shared.remote.exception import RemoteObjectTypeError -from musify.shared.types import Number -from musify.spotify.api import SpotifyAPI -from musify.spotify.object import SpotifyTrack -from tests.shared.core.base import ItemTester -from tests.spotify.api.mock import SpotifyMock -from tests.spotify.utils import assert_id_attributes +from musify.api.exception import APIError +from musify.libraries.remote.core.exception import RemoteObjectTypeError +from musify.types import Number +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.object import SpotifyTrack +from tests.core.base import MusifyItemTester +from tests.libraries.remote.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.utils import assert_id_attributes -class TestSpotifyTrack(ItemTester): +class TestSpotifyTrack(MusifyItemTester): @pytest.fixture def item(self, response_random: dict[str, Any]) -> SpotifyTrack: diff --git a/tests/spotify/object/testers.py b/tests/libraries/remote/spotify/object/testers.py similarity index 89% rename from tests/spotify/object/testers.py rename to tests/libraries/remote/spotify/object/testers.py index 0571684d..0958545f 100644 --- a/tests/spotify/object/testers.py +++ b/tests/libraries/remote/spotify/object/testers.py @@ -5,12 +5,12 @@ import pytest -from musify.shared.remote.enum import RemoteObjectType -from musify.spotify.api import SpotifyAPI -from musify.spotify.base import SpotifyItem, SpotifyObject -from musify.spotify.object import SpotifyCollectionLoader, SpotifyArtist -from tests.shared.remote.object import RemoteCollectionTester -from tests.spotify.api.mock import SpotifyMock +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.base import SpotifyItem, SpotifyObject +from musify.libraries.remote.spotify.object import SpotifyCollectionLoader, SpotifyArtist +from tests.libraries.remote.core.object import RemoteCollectionTester +from tests.libraries.remote.spotify.api.mock import SpotifyMock class SpotifyCollectionLoaderTester(RemoteCollectionTester, metaclass=ABCMeta): diff --git a/tests/spotify/test_library.py b/tests/libraries/remote/spotify/test_library.py similarity index 94% rename from tests/spotify/test_library.py rename to tests/libraries/remote/spotify/test_library.py index 5abde034..51c59d34 100644 --- a/tests/spotify/test_library.py +++ b/tests/libraries/remote/spotify/test_library.py @@ -5,12 +5,12 @@ import pytest from musify.processors.filter import FilterDefinedList, FilterIncludeExclude -from musify.shared.remote.enum import RemoteObjectType -from musify.spotify.api import SpotifyAPI -from musify.spotify.library import SpotifyLibrary -from musify.spotify.object import SpotifyTrack -from tests.shared.remote.library import RemoteLibraryTester -from tests.spotify.api.mock import SpotifyMock +from musify.libraries.remote.core.enum import RemoteObjectType +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.library import SpotifyLibrary +from musify.libraries.remote.spotify.object import SpotifyTrack +from tests.libraries.remote.core.library import RemoteLibraryTester +from tests.libraries.remote.spotify.api.mock import SpotifyMock class TestSpotifyLibrary(RemoteLibraryTester): diff --git a/tests/spotify/test_processors.py b/tests/libraries/remote/spotify/test_processors.py similarity index 80% rename from tests/spotify/test_processors.py rename to tests/libraries/remote/spotify/test_processors.py index e155c98a..b466b7e6 100644 --- a/tests/spotify/test_processors.py +++ b/tests/libraries/remote/spotify/test_processors.py @@ -5,25 +5,25 @@ import pytest -from musify.local.collection import LocalAlbum -from musify.local.track import LocalTrack +from musify.libraries.local.collection import LocalAlbum +from musify.libraries.local.track import LocalTrack from musify.processors.match import CleanTagConfig -from musify.shared.core.enum import TagFields as Tag -from musify.shared.exception import MusifyEnumError -from musify.shared.remote import RemoteResponse -from musify.shared.remote.enum import RemoteIDType as IDType, RemoteObjectType -from musify.shared.remote.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError -from musify.shared.remote.processors.check import RemoteItemChecker -from musify.shared.remote.processors.search import SearchSettings, RemoteItemSearcher -from musify.spotify.api import SpotifyAPI -from musify.spotify.factory import SpotifyObjectFactory -from musify.spotify.object import SpotifyTrack, SpotifyAlbum -from musify.spotify.processors import SpotifyDataWrangler -from tests.local.track.utils import random_track -from tests.shared.remote.processors.check import RemoteItemCheckerTester -from tests.shared.remote.processors.search import RemoteItemSearcherTester -from tests.spotify.api.mock import SpotifyMock -from tests.spotify.utils import random_id, random_ids, random_uri, random_api_url, random_ext_url +from musify.core.enum import TagFields as Tag +from musify.exception import MusifyEnumError +from musify.libraries.remote.core.base import RemoteResponse +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType +from musify.libraries.remote.core.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError +from musify.libraries.remote.core.processors.check import RemoteItemChecker +from musify.libraries.remote.core.processors.search import SearchSettings, RemoteItemSearcher +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.factory import SpotifyObjectFactory +from musify.libraries.remote.spotify.object import SpotifyTrack, SpotifyAlbum +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from tests.libraries.local.track.utils import random_track +from tests.libraries.remote.core.processors.check import RemoteItemCheckerTester +from tests.libraries.remote.core.processors.search import RemoteItemSearcherTester +from tests.libraries.remote.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.utils import random_id, random_ids, random_uri, random_api_url, random_ext_url from tests.utils import random_str @@ -38,11 +38,11 @@ def response(request, api_mock: SpotifyMock) -> RemoteResponse: def test_get_id_type(wrangler: SpotifyDataWrangler): - assert wrangler.get_id_type(random_id()) == IDType.ID - assert wrangler.get_id_type(random_str(1, IDType.ID.value - 1), kind=RemoteObjectType.USER) == IDType.ID - assert wrangler.get_id_type(random_uri()) == IDType.URI - assert wrangler.get_id_type(random_api_url()) == IDType.URL - assert wrangler.get_id_type(random_ext_url()) == IDType.URL_EXT + assert wrangler.get_id_type(random_id()) == RemoteIDType.ID + assert wrangler.get_id_type(random_str(1, RemoteIDType.ID.value - 1), kind=RemoteObjectType.USER) == RemoteIDType.ID + assert wrangler.get_id_type(random_uri()) == RemoteIDType.URI + assert wrangler.get_id_type(random_api_url()) == RemoteIDType.URL + assert wrangler.get_id_type(random_ext_url()) == RemoteIDType.URL_EXT with pytest.raises(RemoteIDTypeError): wrangler.get_id_type("Not an ID") @@ -54,13 +54,13 @@ def test_validate_id_type(wrangler: SpotifyDataWrangler): assert wrangler.validate_id_type(random_api_url()) assert wrangler.validate_id_type(random_ext_url()) - assert wrangler.validate_id_type(random_id(), kind=IDType.ID) - assert wrangler.validate_id_type(random_uri(), kind=IDType.URI) - assert wrangler.validate_id_type(random_api_url(), kind=IDType.URL) - assert wrangler.validate_id_type(random_ext_url(), kind=IDType.URL_EXT) + assert wrangler.validate_id_type(random_id(), kind=RemoteIDType.ID) + assert wrangler.validate_id_type(random_uri(), kind=RemoteIDType.URI) + assert wrangler.validate_id_type(random_api_url(), kind=RemoteIDType.URL) + assert wrangler.validate_id_type(random_ext_url(), kind=RemoteIDType.URL_EXT) - assert not wrangler.validate_id_type(random_id(), kind=IDType.URL) - assert not wrangler.validate_id_type(random_uri(), kind=IDType.URL_EXT) + assert not wrangler.validate_id_type(random_id(), kind=RemoteIDType.URL) + assert not wrangler.validate_id_type(random_uri(), kind=RemoteIDType.URL_EXT) def test_get_item_type(wrangler: SpotifyDataWrangler, object_type: RemoteObjectType): @@ -136,7 +136,7 @@ def test_get_item_type_fails(wrangler: SpotifyDataWrangler): def test_validate_item_type(wrangler: SpotifyDataWrangler, object_type: RemoteObjectType): value = choice([ - random_id() if object_type != RemoteObjectType.USER else random_str(1, IDType.ID.value - 1), + random_id() if object_type != RemoteObjectType.USER else random_str(1, RemoteIDType.ID.value - 1), random_uri(object_type), random_api_url(object_type) + f"/{random_str(0, 10)}", random_ext_url(object_type) + f"/{random_str(0, 10)}", @@ -172,10 +172,10 @@ def test_validate_item_type_response(wrangler: SpotifyDataWrangler, response: Re def test_convert(wrangler: SpotifyDataWrangler, object_type: RemoteObjectType): id_ = random_id() expected_map = { - IDType.ID: id_, - IDType.URI: f"spotify:{object_type.name.lower()}:{id_}", - IDType.URL: f"{wrangler.url_api}/{object_type.name.lower()}s/{id_}", - IDType.URL_EXT: f"{wrangler.url_ext}/{object_type.name.lower()}/{id_}" + RemoteIDType.ID: id_, + RemoteIDType.URI: f"spotify:{object_type.name.lower()}:{id_}", + RemoteIDType.URL: f"{wrangler.url_api}/{object_type.name.lower()}s/{id_}", + RemoteIDType.URL_EXT: f"{wrangler.url_ext}/{object_type.name.lower()}/{id_}" } for type_out, expected in expected_map.items(): for type_in, value in expected_map.items(): @@ -186,10 +186,10 @@ def test_convert(wrangler: SpotifyDataWrangler, object_type: RemoteObjectType): def test_convert_fails(wrangler: SpotifyDataWrangler): # no ID type given when input value is ID raises error with pytest.raises(RemoteIDTypeError): - wrangler.convert(random_id(), type_out=IDType.URI) + wrangler.convert(random_id(), type_out=RemoteIDType.URI) with pytest.raises(RemoteIDTypeError): - wrangler.convert("bad value", type_out=IDType.URI) + wrangler.convert("bad value", type_out=RemoteIDType.URI) def test_extract_ids(wrangler: SpotifyDataWrangler, object_type: RemoteObjectType): diff --git a/tests/spotify/utils.py b/tests/libraries/remote/spotify/utils.py similarity index 89% rename from tests/spotify/utils.py rename to tests/libraries/remote/spotify/utils.py index 2a3b654d..41787901 100644 --- a/tests/spotify/utils.py +++ b/tests/libraries/remote/spotify/utils.py @@ -2,11 +2,11 @@ from random import choice, randrange from typing import Any -from musify.shared.remote.enum import RemoteIDType, RemoteObjectType -from musify.shared.remote.processors.wrangle import RemoteDataWrangler -from musify.spotify.base import SpotifyObject -from musify.spotify.processors import SpotifyDataWrangler -from tests.shared.remote.utils import ALL_ID_TYPES +from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType +from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.spotify.base import SpotifyObject +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from tests.libraries.remote.core.utils import ALL_ID_TYPES from tests.utils import random_str diff --git a/tests/local/playlist/testers.py b/tests/local/playlist/testers.py deleted file mode 100644 index 0e285ff3..00000000 --- a/tests/local/playlist/testers.py +++ /dev/null @@ -1,8 +0,0 @@ -from abc import ABCMeta - -from tests.local.track.testers import LocalCollectionTester -from tests.shared.core.collection import PlaylistTester - - -class LocalPlaylistTester(PlaylistTester, LocalCollectionTester, metaclass=ABCMeta): - pass diff --git a/tests/log/__init__.py b/tests/log/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/shared/test_handlers.py b/tests/log/test_handlers.py similarity index 94% rename from tests/shared/test_handlers.py rename to tests/log/test_handlers.py index 6d24a8e5..ff984a31 100644 --- a/tests/shared/test_handlers.py +++ b/tests/log/test_handlers.py @@ -8,8 +8,8 @@ import pytest -from musify.shared.handlers import CurrentTimeRotatingFileHandler -from musify.shared.logger import LOGGING_DT_FORMAT +from musify.log import LOGGING_DT_FORMAT +from musify.log.handlers import CurrentTimeRotatingFileHandler from tests.utils import random_str diff --git a/tests/shared/test_logger.py b/tests/log/test_logger.py similarity index 93% rename from tests/shared/test_logger.py rename to tests/log/test_logger.py index e60eba73..f36fdd14 100644 --- a/tests/shared/test_logger.py +++ b/tests/log/test_logger.py @@ -5,8 +5,9 @@ import pytest -from musify.shared.logger import MusifyLogger, INFO_EXTRA, REPORT, STAT -from musify.shared.logger import format_full_func_name, LogFileFilter +from musify.log import INFO_EXTRA, REPORT, STAT +from musify.log.filter import format_full_func_name, LogFileFilter +from musify.log.logger import MusifyLogger ########################################################################### diff --git a/tests/processors/test_compare.py b/tests/processors/test_compare.py index 0dd11717..f3b2aca2 100644 --- a/tests/processors/test_compare.py +++ b/tests/processors/test_compare.py @@ -3,15 +3,15 @@ import pytest import xmltodict -from musify.local.track import MP3, M4A, FLAC -from musify.local.track.field import LocalTrackField +from musify.libraries.local.track import MP3, M4A, FLAC +from musify.libraries.local.track.field import LocalTrackField from musify.processors.compare import Comparer from musify.processors.exception import ComparerError, ProcessorLookupError -from musify.shared.field import TrackField -from musify.shared.utils import to_collection -from tests.local.track.utils import random_track -from tests.local.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra -from tests.shared.core.misc import PrettyPrinterTester +from musify.field import TrackField +from musify.utils import to_collection +from tests.libraries.local.track.utils import random_track +from tests.libraries.local.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra +from tests.core.printer import PrettyPrinterTester class TestComparer(PrettyPrinterTester): diff --git a/tests/processors/test_download.py b/tests/processors/test_download.py index 2f7ae3fc..ad35b288 100644 --- a/tests/processors/test_download.py +++ b/tests/processors/test_download.py @@ -5,13 +5,13 @@ from pytest_mock import MockerFixture from musify import MODULE_ROOT -from musify.local.track import LocalTrack +from musify.libraries.local.track import LocalTrack from musify.processors.download import ItemDownloadHelper -from musify.shared.core.enum import Fields -from musify.shared.core.object import BasicCollection -from tests.local.track.utils import random_tracks -from tests.shared.core.misc import PrettyPrinterTester -from tests.shared.remote.processors.utils import patch_input +from musify.core.enum import Fields +from musify.libraries.collection import BasicCollection +from tests.libraries.local.track.utils import random_tracks +from tests.core.printer import PrettyPrinterTester +from tests.libraries.remote.core.processors.utils import patch_input from tests.utils import random_str, get_stdout diff --git a/tests/processors/test_filter.py b/tests/processors/test_filter.py index ac728228..2ad5fc1d 100644 --- a/tests/processors/test_filter.py +++ b/tests/processors/test_filter.py @@ -6,18 +6,18 @@ import pytest import xmltodict -from musify.local.track import LocalTrack -from musify.local.track.field import LocalTrackField +from musify.libraries.local.track import LocalTrack +from musify.libraries.local.track.field import LocalTrackField from musify.processors.compare import Comparer from musify.processors.filter import FilterDefinedList, FilterComparers, FilterIncludeExclude from musify.processors.filter_matcher import FilterMatcher -from musify.shared.core.enum import Fields -from musify.shared.file import PathStemMapper, PathMapper -from tests.local.track.utils import random_tracks -from tests.local.utils import path_playlist_resources -from tests.local.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra, path_playlist_xautopf_cm -from tests.local.utils import path_track_all, path_track_wma, path_track_flac, path_track_mp3 -from tests.shared.core.misc import PrettyPrinterTester +from musify.core.enum import Fields +from musify.file.path_mapper import PathStemMapper, PathMapper +from tests.libraries.local.track.utils import random_tracks +from tests.libraries.local.utils import path_playlist_resources +from tests.libraries.local.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra, path_playlist_xautopf_cm +from tests.libraries.local.utils import path_track_all, path_track_wma, path_track_flac, path_track_mp3 +from tests.core.printer import PrettyPrinterTester from tests.utils import random_str, path_resources diff --git a/tests/processors/test_limit.py b/tests/processors/test_limit.py index 3574ab05..876111b8 100644 --- a/tests/processors/test_limit.py +++ b/tests/processors/test_limit.py @@ -4,11 +4,11 @@ import pytest import xmltodict -from musify.local.track import LocalTrack +from musify.libraries.local.track import LocalTrack from musify.processors.limit import ItemLimiter, LimitType -from tests.local.track.utils import random_tracks -from tests.local.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra -from tests.shared.core.misc import PrettyPrinterTester +from tests.libraries.local.track.utils import random_tracks +from tests.libraries.local.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra +from tests.core.printer import PrettyPrinterTester from tests.utils import random_file diff --git a/tests/processors/test_match.py b/tests/processors/test_match.py index 529f971e..ee959121 100644 --- a/tests/processors/test_match.py +++ b/tests/processors/test_match.py @@ -1,10 +1,10 @@ import pytest -from musify.local.track import LocalTrack +from musify.libraries.local.track import LocalTrack from musify.processors.match import ItemMatcher, CleanTagConfig -from musify.shared.core.enum import TagFields as Tag -from tests.local.track.utils import random_track -from tests.shared.core.misc import PrettyPrinterTester +from musify.core.enum import TagFields as Tag +from tests.libraries.local.track.utils import random_track +from tests.core.printer import PrettyPrinterTester class TestItemMatcher(PrettyPrinterTester): diff --git a/tests/processors/test_sort.py b/tests/processors/test_sort.py index 543b55d0..92303901 100644 --- a/tests/processors/test_sort.py +++ b/tests/processors/test_sort.py @@ -5,14 +5,14 @@ import pytest import xmltodict -from musify.local.track import LocalTrack -from musify.local.track.field import LocalTrackField +from musify.libraries.local.track import LocalTrack +from musify.libraries.local.track.field import LocalTrackField from musify.processors.sort import ItemSorter, ShuffleMode, ShuffleBy -from musify.shared.field import TrackField -from musify.shared.utils import strip_ignore_words -from tests.local.track.utils import random_tracks -from tests.local.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra -from tests.shared.core.misc import PrettyPrinterTester +from musify.field import TrackField +from musify.utils import strip_ignore_words +from tests.libraries.local.track.utils import random_tracks +from tests.libraries.local.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra +from tests.core.printer import PrettyPrinterTester class TestItemSorter(PrettyPrinterTester): diff --git a/tests/processors/test_time.py b/tests/processors/test_time.py index 80b463a3..bd793077 100644 --- a/tests/processors/test_time.py +++ b/tests/processors/test_time.py @@ -4,7 +4,7 @@ from dateutil.relativedelta import relativedelta from musify.processors.time import TimeMapper -from tests.shared.core.misc import PrettyPrinterTester +from tests.core.printer import PrettyPrinterTester class TestTimeMapper(PrettyPrinterTester): diff --git a/tests/shared/test_fields.py b/tests/test_fields.py similarity index 82% rename from tests/shared/test_fields.py rename to tests/test_fields.py index c2e2ef12..76f17d41 100644 --- a/tests/shared/test_fields.py +++ b/tests/test_fields.py @@ -1,11 +1,11 @@ import pytest -from musify.local.track import LocalTrack -from musify.local.track.field import LocalTrackField -from musify.shared.core.object import Track, Playlist, Folder, Artist, Album -from musify.shared.field import FolderField, PlaylistField, AlbumField, ArtistField -from musify.shared.field import TrackField -from tests.shared.core.enums import FieldTester, TagFieldTester +from musify.libraries.local.track import LocalTrack +from musify.libraries.local.track.field import LocalTrackField +from musify.libraries.core.object import Track, Playlist, Folder, Artist, Album +from musify.field import FolderField, PlaylistField, AlbumField, ArtistField +from musify.field import TrackField +from tests.core.enum import FieldTester, TagFieldTester class TestTrackField(TagFieldTester): diff --git a/tests/test_report.py b/tests/test_report.py index 3d914758..dc78d191 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -5,17 +5,17 @@ import pytest -from musify.local.library import LocalLibrary -from musify.local.playlist import M3U -from musify.local.track.field import LocalTrackField +from musify.libraries.local.library import LocalLibrary +from musify.libraries.local.playlist import M3U +from musify.libraries.local.track.field import LocalTrackField from musify.report import report_playlist_differences, report_missing_tags -from musify.spotify.api import SpotifyAPI -from musify.spotify.library import SpotifyLibrary -from musify.spotify.object import SpotifyPlaylist -from musify.spotify.processors import SpotifyDataWrangler -from tests.local.track.utils import random_track -from tests.spotify.api.mock import SpotifyMock -from tests.spotify.utils import random_uri +from musify.libraries.remote.spotify.api import SpotifyAPI +from musify.libraries.remote.spotify.library import SpotifyLibrary +from musify.libraries.remote.spotify.object import SpotifyPlaylist +from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from tests.libraries.local.track.utils import random_track +from tests.libraries.remote.spotify.api.mock import SpotifyMock +from tests.libraries.remote.spotify.utils import random_uri @pytest.fixture diff --git a/tests/shared/test_utils.py b/tests/test_utils.py similarity index 95% rename from tests/shared/test_utils.py rename to tests/test_utils.py index f5616a7b..8a17422e 100644 --- a/tests/shared/test_utils.py +++ b/tests/test_utils.py @@ -2,10 +2,10 @@ import pytest -from musify.shared.exception import MusifyTypeError -from musify.shared.utils import flatten_nested, merge_maps, get_most_common_values, unicode_len -from musify.shared.utils import limit_value, to_collection, unique_list -from musify.shared.utils import strip_ignore_words, safe_format_map, get_max_width, align_string +from musify.exception import MusifyTypeError +from musify.utils import flatten_nested, merge_maps, get_most_common_values, unicode_len +from musify.utils import limit_value, to_collection, unique_list +from musify.utils import strip_ignore_words, safe_format_map, get_max_width, align_string ########################################################################### diff --git a/tests/utils.py b/tests/utils.py index 05027df9..fa36018c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -9,7 +9,7 @@ import pytest -from musify.shared.core.enum import MusifyEnum +from musify.core.enum import MusifyEnum path_root = dirname(dirname(__file__)) path_tests = dirname(__file__)