From 510b1c61dd74b8f5dc1e134a293c7ba2c2b3b1b3 Mon Sep 17 00:00:00 2001 From: geo-martino Date: Sat, 6 Jan 2024 01:40:31 -0500 Subject: [PATCH] restructure shared modules + fix documentation --- .gitignore | 2 +- .idea/syncify.iml | 53 +++++----- logging.yml | 8 +- main.py | 21 ++-- readme.py | 2 +- src/syncify/__init__.py | 5 +- src/syncify/abstract/__init__.py | 1 - src/syncify/api/__init__.py | 2 - src/syncify/config.py | 37 +++---- src/syncify/local/__init__.py | 2 - src/syncify/local/{_base.py => base.py} | 4 +- src/syncify/local/collection.py | 25 ++--- src/syncify/local/exception.py | 2 +- src/syncify/local/{_file.py => file.py} | 0 src/syncify/local/library/__init__.py | 4 +- .../local/library/{_library.py => library.py} | 16 +-- .../library/{_musicbee.py => musicbee.py} | 10 +- src/syncify/local/playlist/__init__.py | 10 +- .../local/playlist/{_playlist.py => base.py} | 10 +- .../local/playlist/{_m3u.py => m3u.py} | 10 +- .../local/playlist/{_match.py => match.py} | 8 +- .../local/playlist/{_utils.py => utils.py} | 10 +- .../playlist/{_xautopf.py => xautopf.py} | 12 +-- src/syncify/local/track/__init__.py | 10 +- src/syncify/local/track/_base/reader.py | 18 ++-- src/syncify/local/track/_base/track.py | 14 +-- src/syncify/local/track/_base/writer.py | 8 +- src/syncify/local/track/fields.py | 53 ++++++++++ src/syncify/local/track/{_flac.py => flac.py} | 8 +- src/syncify/local/track/{_m4a.py => m4a.py} | 8 +- src/syncify/local/track/{_mp3.py => mp3.py} | 8 +- .../local/track/{_utils.py => utils.py} | 10 +- src/syncify/local/track/{_wma.py => wma.py} | 6 +- src/syncify/{utils => }/printers.py | 0 src/syncify/processors/base.py | 2 +- src/syncify/processors/compare.py | 10 +- src/syncify/processors/exception.py | 2 +- src/syncify/processors/limit.py | 8 +- src/syncify/processors/match.py | 16 +-- src/syncify/processors/sort.py | 8 +- src/syncify/processors/time.py | 2 +- src/syncify/report.py | 13 +-- .../{remote/processors => shared}/__init__.py | 0 src/syncify/shared/api/__init__.py | 0 .../_authorise.py => shared/api/authorise.py} | 4 +- src/syncify/{ => shared}/api/exception.py | 2 +- .../_request.py => shared/api/request.py} | 4 +- src/syncify/shared/core/__init__.py | 0 .../_base.py => shared/core/base.py} | 4 +- .../{abstract => shared/core}/collection.py | 8 +- .../{abstract => shared/core}/enums.py | 6 +- src/syncify/{abstract => shared/core}/misc.py | 2 +- .../{abstract => shared/core}/object.py | 12 +-- src/syncify/{ => shared}/exception.py | 3 +- src/syncify/{ => shared}/fields.py | 53 +--------- src/syncify/{utils => shared}/logger.py | 3 +- src/syncify/{ => shared}/remote/__init__.py | 0 src/syncify/{ => shared}/remote/_base.py | 0 src/syncify/{ => shared}/remote/api.py | 12 +-- src/syncify/{ => shared}/remote/base.py | 8 +- src/syncify/{ => shared}/remote/config.py | 2 +- src/syncify/{ => shared}/remote/enums.py | 2 +- src/syncify/{ => shared}/remote/exception.py | 4 +- src/syncify/{ => shared}/remote/library.py | 16 +-- src/syncify/{ => shared}/remote/object.py | 24 ++--- .../shared/remote/processors/__init__.py | 0 .../{ => shared}/remote/processors/check.py | 24 ++--- .../{ => shared}/remote/processors/search.py | 22 ++--- .../{ => shared}/remote/processors/wrangle.py | 8 +- src/syncify/{ => shared}/remote/types.py | 2 +- src/syncify/shared/types.py | 10 ++ .../{utils/helpers.py => shared/utils.py} | 15 +-- src/syncify/spotify/_base.py | 2 +- src/syncify/spotify/api/__init__.py | 2 +- src/syncify/spotify/api/{_api.py => api.py} | 10 +- src/syncify/spotify/api/{_base.py => base.py} | 2 +- src/syncify/spotify/api/{_item.py => item.py} | 12 +-- src/syncify/spotify/api/{_core.py => misc.py} | 8 +- .../spotify/api/{_playlist.py => playlist.py} | 8 +- src/syncify/spotify/base.py | 8 +- src/syncify/spotify/config.py | 2 +- src/syncify/spotify/exception.py | 2 +- src/syncify/spotify/library.py | 10 +- src/syncify/spotify/object.py | 10 +- src/syncify/spotify/processors/processors.py | 6 +- src/syncify/spotify/processors/wrangle.py | 10 +- src/syncify/utils/__init__.py | 1 - tests/__resources/test_logging.yml | 6 +- tests/conftest.py | 20 ++-- tests/local/conftest.py | 2 +- tests/local/library/test_local_library.py | 7 +- tests/local/library/test_musicbee.py | 12 +-- tests/local/library/testers.py | 42 ++++++++ tests/local/library/utils.py | 42 +------- .../playlist/test_local_playlist_match.py | 7 +- tests/local/playlist/test_m3u.py | 6 +- tests/local/playlist/test_xautopf.py | 8 +- tests/local/playlist/testers.py | 8 ++ tests/local/playlist/utils.py | 7 -- tests/local/test_local_collection.py | 62 +----------- tests/local/{ => track}/test_track.py | 14 +-- tests/local/track/testers.py | 63 ++++++++++++ tests/local/track/utils.py | 75 ++++++++++++++ tests/local/utils.py | 74 +------------- tests/processors/test_compare.py | 7 +- tests/processors/test_limit.py | 4 +- tests/processors/test_match.py | 6 +- tests/processors/test_sort.py | 9 +- tests/processors/test_time.py | 2 +- tests/{ => shared}/api/test_authorise.py | 8 +- tests/{ => shared}/api/test_request.py | 4 +- tests/{ => shared}/api/utils.py | 0 tests/{abstract => shared/core}/base.py | 6 +- tests/{abstract => shared/core}/collection.py | 16 +-- tests/{abstract => shared/core}/enums.py | 4 +- tests/{abstract => shared/core}/misc.py | 5 +- .../library => shared/remote}/library.py | 16 +-- .../library => shared/remote}/object.py | 14 +-- tests/{ => shared}/remote/processors/check.py | 14 +-- .../{ => shared}/remote/processors/search.py | 16 +-- tests/{ => shared}/remote/utils.py | 33 +------ tests/{ => shared}/test_fields.py | 9 +- tests/{utils => shared}/test_helpers.py | 8 +- tests/{utils => shared}/test_logger.py | 7 +- tests/spotify/api/mock.py | 6 +- tests/spotify/api/test_spotify_api_item.py | 11 ++- ...y_api_core.py => test_spotify_api_misc.py} | 2 +- .../spotify/api/test_spotify_api_playlist.py | 9 +- tests/spotify/library/utils.py | 12 --- .../{library => }/test_spotify_collection.py | 98 ++----------------- .../{library => }/test_spotify_library.py | 4 +- .../{library => }/test_spotify_playlist.py | 13 ++- tests/spotify/test_spotify_processors.py | 16 +-- .../{library => }/test_spotify_track.py | 8 +- tests/spotify/testers.py | 96 ++++++++++++++++++ tests/spotify/utils.py | 48 ++++++++- tests/test_config.py | 15 ++- tests/test_report.py | 4 +- 138 files changed, 916 insertions(+), 870 deletions(-) delete mode 100644 src/syncify/abstract/__init__.py delete mode 100644 src/syncify/api/__init__.py rename src/syncify/local/{_base.py => base.py} (73%) rename src/syncify/local/{_file.py => file.py} (100%) rename src/syncify/local/library/{_library.py => library.py} (95%) rename src/syncify/local/library/{_musicbee.py => musicbee.py} (96%) rename src/syncify/local/playlist/{_playlist.py => base.py} (94%) rename src/syncify/local/playlist/{_m3u.py => m3u.py} (93%) rename src/syncify/local/playlist/{_match.py => match.py} (98%) rename src/syncify/local/playlist/{_utils.py => utils.py} (90%) rename src/syncify/local/playlist/{_xautopf.py => xautopf.py} (94%) create mode 100644 src/syncify/local/track/fields.py rename src/syncify/local/track/{_flac.py => flac.py} (95%) rename src/syncify/local/track/{_m4a.py => m4a.py} (97%) rename src/syncify/local/track/{_mp3.py => mp3.py} (97%) rename src/syncify/local/track/{_utils.py => utils.py} (87%) rename src/syncify/local/track/{_wma.py => wma.py} (97%) rename src/syncify/{utils => }/printers.py (100%) rename src/syncify/{remote/processors => shared}/__init__.py (100%) create mode 100644 src/syncify/shared/api/__init__.py rename src/syncify/{api/_authorise.py => shared/api/authorise.py} (97%) rename src/syncify/{ => shared}/api/exception.py (89%) rename src/syncify/{api/_request.py => shared/api/request.py} (96%) create mode 100644 src/syncify/shared/core/__init__.py rename src/syncify/{abstract/_base.py => shared/core/base.py} (92%) rename src/syncify/{abstract => shared/core}/collection.py (94%) rename src/syncify/{abstract => shared/core}/enums.py (95%) rename src/syncify/{abstract => shared/core}/misc.py (97%) rename src/syncify/{abstract => shared/core}/object.py (95%) rename src/syncify/{ => shared}/exception.py (94%) rename src/syncify/{ => shared}/fields.py (64%) rename src/syncify/{utils => shared}/logger.py (96%) rename src/syncify/{ => shared}/remote/__init__.py (100%) rename src/syncify/{ => shared}/remote/_base.py (100%) rename src/syncify/{ => shared}/remote/api.py (95%) rename src/syncify/{ => shared}/remote/base.py (92%) rename src/syncify/{ => shared}/remote/config.py (70%) rename src/syncify/{ => shared}/remote/enums.py (86%) rename src/syncify/{ => shared}/remote/exception.py (89%) rename src/syncify/{ => shared}/remote/library.py (97%) rename src/syncify/{ => shared}/remote/object.py (95%) create mode 100644 src/syncify/shared/remote/processors/__init__.py rename src/syncify/{ => shared}/remote/processors/check.py (94%) rename src/syncify/{ => shared}/remote/processors/search.py (93%) rename src/syncify/{ => shared}/remote/processors/wrangle.py (95%) rename src/syncify/{ => shared}/remote/types.py (73%) create mode 100644 src/syncify/shared/types.py rename src/syncify/{utils/helpers.py => shared/utils.py} (92%) rename src/syncify/spotify/api/{_api.py => api.py} (89%) rename src/syncify/spotify/api/{_base.py => base.py} (90%) rename src/syncify/spotify/api/{_item.py => item.py} (96%) rename src/syncify/spotify/api/{_core.py => misc.py} (93%) rename src/syncify/spotify/api/{_playlist.py => playlist.py} (95%) delete mode 100644 src/syncify/utils/__init__.py create mode 100644 tests/local/library/testers.py create mode 100644 tests/local/playlist/testers.py rename tests/local/{ => track}/test_track.py (95%) create mode 100644 tests/local/track/testers.py create mode 100644 tests/local/track/utils.py rename tests/{ => shared}/api/test_authorise.py (96%) rename tests/{ => shared}/api/test_request.py (96%) rename tests/{ => shared}/api/utils.py (100%) rename tests/{abstract => shared/core}/base.py (87%) rename tests/{abstract => shared/core}/collection.py (93%) rename tests/{abstract => shared/core}/enums.py (93%) rename tests/{abstract => shared/core}/misc.py (89%) rename tests/{remote/library => shared/remote}/library.py (94%) rename tests/{remote/library => shared/remote}/object.py (94%) rename tests/{ => shared}/remote/processors/check.py (95%) rename tests/{ => shared}/remote/processors/search.py (93%) rename tests/{ => shared}/remote/utils.py (70%) rename tests/{ => shared}/test_fields.py (84%) rename tests/{utils => shared}/test_helpers.py (93%) rename tests/{utils => shared}/test_logger.py (94%) rename tests/spotify/api/{test_spotify_api_core.py => test_spotify_api_misc.py} (96%) delete mode 100644 tests/spotify/library/utils.py rename tests/spotify/{library => }/test_spotify_collection.py (78%) rename tests/spotify/{library => }/test_spotify_library.py (96%) rename tests/spotify/{library => }/test_spotify_playlist.py (94%) rename tests/spotify/{library => }/test_spotify_track.py (95%) create mode 100644 tests/spotify/testers.py diff --git a/.gitignore b/.gitignore index b2c0a67a..fce1c467 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ config.yml # build/dev/doc directories dist/ .grip/ -_site/ +.docs/ docs/modules.rst docs/syncify*.rst diff --git a/.idea/syncify.iml b/.idea/syncify.iml index 3c7ef020..ae37acee 100644 --- a/.idea/syncify.iml +++ b/.idea/syncify.iml @@ -1,27 +1,28 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/logging.yml b/logging.yml index fc81c10a..b0445b57 100644 --- a/logging.yml +++ b/logging.yml @@ -16,10 +16,10 @@ formatters: filters: console: - (): "syncify.utils.logger.LogConsoleFilter" + (): "syncify.shared.logger.LogConsoleFilter" module_width: 40 file: - (): "syncify.utils.logger.LogFileFilter" + (): "syncify.shared.logger.LogFileFilter" module_width: 40 handlers: @@ -50,7 +50,7 @@ handlers: stream: ext://sys.stdout file: - class: syncify.utils.logger.CurrentTimeRotatingFileHandler + class: syncify.shared.logger.CurrentTimeRotatingFileHandler level: DEBUG formatter: extended filters: ["file"] @@ -69,7 +69,7 @@ loggers: test: <<: *logger - handlers: [console_stat] + handlers: [console_debug] dev: <<: *logger diff --git a/main.py b/main.py index fa6dec88..edb452f4 100644 --- a/main.py +++ b/main.py @@ -5,26 +5,27 @@ import re import sys import traceback -from collections.abc import Mapping +from collections.abc import Mapping, Callable from datetime import date, datetime, timedelta from os.path import basename, dirname, join, relpath, splitext from time import perf_counter -from typing import Any, Callable +from typing import Any from syncify import PROGRAM_NAME from syncify.config import Config, ConfigLibraryDifferences, ConfigMissingTags, ConfigRemote, ConfigLocal -from syncify.exception import ConfigError -from syncify.fields import LocalTrackField +from syncify.shared.exception import ConfigError +from syncify.local.track.fields import LocalTrackField from syncify.local.collection import LocalCollection from syncify.local.track import LocalTrack, SyncResultTrack from syncify.processors.base import DynamicProcessor, dynamicprocessormethod -from syncify.remote.api import RemoteAPI -from syncify.remote.enums import RemoteObjectType -from syncify.remote.object import RemoteAlbum +from syncify.shared.remote.api import RemoteAPI +from syncify.shared.remote.enums import RemoteObjectType +from syncify.shared.remote.object import RemoteAlbum from syncify.report import report_playlist_differences, report_missing_tags -from syncify.utils.helpers import get_user_input, UnitIterable, to_collection -from syncify.utils.logger import SyncifyLogger, STAT, CurrentTimeRotatingFileHandler, REPORT -from syncify.utils.printers import print_logo, print_line, print_time +from syncify.shared.utils import get_user_input, to_collection +from syncify.shared.types import UnitIterable +from syncify.shared.logger import SyncifyLogger, STAT, CurrentTimeRotatingFileHandler, REPORT +from syncify.printers import print_logo, print_line, print_time class Syncify(DynamicProcessor): diff --git a/readme.py b/readme.py index 4c804961..ec6d2d1b 100644 --- a/readme.py +++ b/readme.py @@ -1,4 +1,4 @@ -from syncify.exception import SafeDict +from syncify.shared.exception import SafeDict from syncify.local.track import TRACK_FILETYPES from syncify.local.playlist import PLAYLIST_FILETYPES from syncify.local.library import LIBRARY_CLASSES, LocalLibrary diff --git a/src/syncify/__init__.py b/src/syncify/__init__.py index 6caee0e7..2b4b6842 100644 --- a/src/syncify/__init__.py +++ b/src/syncify/__init__.py @@ -2,9 +2,10 @@ PROGRAM_NAME = "Syncify" __version__ = "0.3" -PROGRAM_OWNER_NAME = "geo-martino" +PROGRAM_OWNER_NAME = "George Martin Marino" +PROGRAM_OWNER_USER = "geo-martino" PROGRAM_OWNER_EMAIL = f"gm.engineer+{PROGRAM_NAME.lower()}@pm.me" -PROGRAM_URL = f"https://github.com/{PROGRAM_OWNER_NAME}/{PROGRAM_NAME.casefold()}" +PROGRAM_URL = f"https://github.com/{PROGRAM_OWNER_USER}/{PROGRAM_NAME.casefold()}" MODULE_ROOT: str = basename(dirname(__file__)) PACKAGE_ROOT: str = dirname(dirname(dirname(__file__))) diff --git a/src/syncify/abstract/__init__.py b/src/syncify/abstract/__init__.py deleted file mode 100644 index e938cb29..00000000 --- a/src/syncify/abstract/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from ._base import NamedObject, NamedObjectPrinter, Item diff --git a/src/syncify/api/__init__.py b/src/syncify/api/__init__.py deleted file mode 100644 index c0713ec5..00000000 --- a/src/syncify/api/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from ._authorise import APIAuthoriser -from ._request import RequestHandler diff --git a/src/syncify/config.py b/src/syncify/config.py index 3d12cce5..10a9de40 100644 --- a/src/syncify/config.py +++ b/src/syncify/config.py @@ -6,34 +6,35 @@ import os import sys from abc import ABCMeta, abstractmethod -from collections.abc import Mapping, Collection, Callable +from collections.abc import Mapping, Collection, Callable, Iterable from copy import deepcopy from dataclasses import dataclass from datetime import datetime from os.path import isabs, join, dirname, splitext, exists -from typing import Any, Self, get_args, Iterable +from typing import Any, Self, get_args import yaml from syncify import PACKAGE_ROOT, MODULE_ROOT -from syncify.abstract import NamedObject -from syncify.abstract.enums import TagField -from syncify.abstract.misc import PrettyPrinter, Filter -from syncify.abstract.object import Library -from syncify.api import APIAuthoriser, RequestHandler -from syncify.exception import ConfigError, SyncifyError -from syncify.fields import LocalTrackField +from syncify.shared.core.base import NamedObject +from syncify.shared.core.enums import TagField +from syncify.shared.core.misc import PrettyPrinter, Filter +from syncify.shared.core.object import Library +from syncify.shared.api.authorise import APIAuthoriser +from syncify.shared.api.request import RequestHandler +from syncify.shared.exception import ConfigError, SyncifyError +from syncify.local.track.fields import LocalTrackField from syncify.local.collection import LocalCollection from syncify.local.exception import InvalidFileType, FileDoesNotExistError from syncify.local.library import MusicBee, LocalLibrary from syncify.processors.compare import Comparer -from syncify.remote.api import RemoteAPI -from syncify.remote.base import RemoteObject -from syncify.remote.library import RemoteLibrary -from syncify.remote.object import PLAYLIST_SYNC_KINDS, RemotePlaylist -from syncify.remote.processors.check import RemoteItemChecker -from syncify.remote.processors.search import RemoteItemSearcher -from syncify.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.remote.api import RemoteAPI +from syncify.shared.remote.base import RemoteObject +from syncify.shared.remote.library import RemoteLibrary +from syncify.shared.remote.object import PLAYLIST_SYNC_KINDS, RemotePlaylist +from syncify.shared.remote.processors.check import RemoteItemChecker +from syncify.shared.remote.processors.search import RemoteItemSearcher +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler from syncify.report import report_missing_tags from syncify.spotify import SPOTIFY_NAME from syncify.spotify.api import SpotifyAPI @@ -42,8 +43,8 @@ from syncify.spotify.object import SpotifyPlaylist from syncify.spotify.processors.processors import SpotifyItemChecker, SpotifyItemSearcher from syncify.spotify.processors.wrangle import SpotifyDataWrangler -from syncify.utils.helpers import to_collection -from syncify.utils.logger import LOGGING_DT_FORMAT, SyncifyLogger +from syncify.shared.utils import to_collection +from syncify.shared.logger import LOGGING_DT_FORMAT, SyncifyLogger def _get_local_track_tags(tags: Any) -> tuple[LocalTrackField, ...]: diff --git a/src/syncify/local/__init__.py b/src/syncify/local/__init__.py index 1142bca1..e69de29b 100644 --- a/src/syncify/local/__init__.py +++ b/src/syncify/local/__init__.py @@ -1,2 +0,0 @@ -from ._base import LocalItem -from ._file import File, open_image, get_image_bytes diff --git a/src/syncify/local/_base.py b/src/syncify/local/base.py similarity index 73% rename from src/syncify/local/_base.py rename to src/syncify/local/base.py index 58103348..d3965756 100644 --- a/src/syncify/local/_base.py +++ b/src/syncify/local/base.py @@ -1,7 +1,7 @@ from abc import ABCMeta -from syncify.abstract import Item -from syncify.local._file import File +from syncify.shared.core.base import Item +from syncify.local.file import File class LocalItem(File, Item, metaclass=ABCMeta): diff --git a/src/syncify/local/collection.py b/src/syncify/local/collection.py index 3d5eae08..c51ecf45 100644 --- a/src/syncify/local/collection.py +++ b/src/syncify/local/collection.py @@ -9,20 +9,21 @@ from os.path import splitext, join, basename, exists, isdir from typing import Any -from syncify.abstract import Item -from syncify.abstract.collection import ItemCollection -from syncify.abstract.enums import Fields, TagField, TagFields -from syncify.abstract.object import Track, Library, Folder, Album, Artist, Genre -from syncify.exception import SyncifyKeyError -from syncify.fields import LocalTrackField -from syncify.local._base import LocalItem +from syncify.shared.core.base import Item +from syncify.shared.core.collection import ItemCollection +from syncify.shared.core.enums import Fields, TagField, TagFields +from syncify.shared.core.object import Track, Library, Folder, Album, Artist, Genre +from syncify.shared.exception import SyncifyKeyError +from syncify.local.track.fields import LocalTrackField +from syncify.local.base import LocalItem from syncify.local.exception import LocalCollectionError from syncify.local.track import LocalTrack, SyncResultTrack, load_track, TRACK_FILETYPES -from syncify.remote.enums import RemoteIDType -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils import UnitCollection -from syncify.utils.helpers import get_most_common_values, to_collection, UnitIterable, align_and_truncate, get_max_width -from syncify.utils.logger import SyncifyLogger, STAT +from syncify.shared.remote.enums import RemoteIDType +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.types import UnitCollection +from syncify.shared.utils import get_most_common_values, to_collection, align_and_truncate, get_max_width +from syncify.shared.types import UnitIterable +from syncify.shared.logger import SyncifyLogger, STAT __max_str = "z" * 50 diff --git a/src/syncify/local/exception.py b/src/syncify/local/exception.py index 6f3194cb..37f97287 100644 --- a/src/syncify/local/exception.py +++ b/src/syncify/local/exception.py @@ -1,6 +1,6 @@ from typing import Any -from syncify.exception import SyncifyError +from syncify.shared.exception import SyncifyError class LocalError(SyncifyError): diff --git a/src/syncify/local/_file.py b/src/syncify/local/file.py similarity index 100% rename from src/syncify/local/_file.py rename to src/syncify/local/file.py diff --git a/src/syncify/local/library/__init__.py b/src/syncify/local/library/__init__.py index 7200df39..6dabc729 100644 --- a/src/syncify/local/library/__init__.py +++ b/src/syncify/local/library/__init__.py @@ -1,4 +1,4 @@ -from ._library import LocalLibrary -from ._musicbee import MusicBee +from .library import LocalLibrary +from .musicbee import MusicBee LIBRARY_CLASSES = frozenset({LocalLibrary, MusicBee}) diff --git a/src/syncify/local/library/_library.py b/src/syncify/local/library/library.py similarity index 95% rename from src/syncify/local/library/_library.py rename to src/syncify/local/library/library.py index 3a1f1a74..95caee9b 100644 --- a/src/syncify/local/library/_library.py +++ b/src/syncify/local/library/library.py @@ -3,19 +3,19 @@ from os.path import splitext, join, exists, basename from typing import Any -from syncify.abstract.misc import Result, Filter -from syncify.abstract.object import Playlist, Library -from syncify.exception import SyncifyError -from syncify.fields import LocalTrackField +from syncify.shared.core.misc import Result, Filter +from syncify.shared.core.object import Playlist, Library +from syncify.shared.exception import SyncifyError +from syncify.local.track.fields import LocalTrackField from syncify.local.collection import LocalCollection, LocalFolder, LocalAlbum, LocalArtist, LocalGenres from syncify.local.exception import LocalCollectionError from syncify.local.playlist import PLAYLIST_FILETYPES, LocalPlaylist, load_playlist from syncify.local.track import TRACK_CLASSES, LocalTrack, load_track from syncify.processors.sort import ItemSorter -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils import UnitCollection, UnitIterable -from syncify.utils.helpers import align_and_truncate, get_max_width, correct_platform_separators -from syncify.utils.logger import REPORT +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.types import UnitCollection, UnitIterable +from syncify.shared.utils import align_and_truncate, get_max_width, correct_platform_separators +from syncify.shared.logger import REPORT class LocalLibrary(LocalCollection[LocalTrack], Library[LocalTrack]): diff --git a/src/syncify/local/library/_musicbee.py b/src/syncify/local/library/musicbee.py similarity index 96% rename from src/syncify/local/library/_musicbee.py rename to src/syncify/local/library/musicbee.py index 1dd01cac..1460b434 100644 --- a/src/syncify/local/library/_musicbee.py +++ b/src/syncify/local/library/musicbee.py @@ -9,14 +9,14 @@ from lxml import etree from lxml.etree import iterparse -from syncify.local import File +from syncify.local.file import File from syncify.local.exception import MusicBeeIDError, XMLReaderError, MusicBeeError, FileDoesNotExistError -from syncify.local.library._library import LocalLibrary +from syncify.local.library.library import LocalLibrary from syncify.local.playlist import LocalPlaylist from syncify.local.track import LocalTrack -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils import UnitCollection, Number -from syncify.utils.helpers import correct_platform_separators +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.types import UnitCollection, Number +from syncify.shared.utils import correct_platform_separators class MusicBee(LocalLibrary, File): diff --git a/src/syncify/local/playlist/__init__.py b/src/syncify/local/playlist/__init__.py index d3b91b41..16057b7e 100644 --- a/src/syncify/local/playlist/__init__.py +++ b/src/syncify/local/playlist/__init__.py @@ -1,5 +1,5 @@ -from ._m3u import M3U -from ._match import LocalMatcher -from ._playlist import LocalPlaylist -from ._utils import PLAYLIST_CLASSES, PLAYLIST_FILETYPES, load_playlist -from ._xautopf import XAutoPF +from .m3u import M3U +from .match import LocalMatcher +from .base import LocalPlaylist +from .utils import PLAYLIST_CLASSES, PLAYLIST_FILETYPES, load_playlist +from .xautopf import XAutoPF diff --git a/src/syncify/local/playlist/_playlist.py b/src/syncify/local/playlist/base.py similarity index 94% rename from src/syncify/local/playlist/_playlist.py rename to src/syncify/local/playlist/base.py index 7f2505f9..d8581961 100644 --- a/src/syncify/local/playlist/_playlist.py +++ b/src/syncify/local/playlist/base.py @@ -4,15 +4,15 @@ from datetime import datetime from os.path import dirname, join, getmtime, getctime, exists -from syncify.abstract.misc import Result -from syncify.abstract.object import Playlist -from syncify.local import File +from syncify.shared.core.misc import Result +from syncify.shared.core.object import Playlist +from syncify.local.file import File from syncify.local.collection import LocalCollection -from syncify.local.playlist._match import LocalMatcher +from syncify.local.playlist.match import LocalMatcher from syncify.local.track import LocalTrack, load_track from syncify.processors.limit import ItemLimiter from syncify.processors.sort import ItemSorter -from syncify.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler class LocalPlaylist(LocalCollection[LocalTrack], Playlist[LocalTrack], File, metaclass=ABCMeta): diff --git a/src/syncify/local/playlist/_m3u.py b/src/syncify/local/playlist/m3u.py similarity index 93% rename from src/syncify/local/playlist/_m3u.py rename to src/syncify/local/playlist/m3u.py index df125ef2..394d4e04 100644 --- a/src/syncify/local/playlist/_m3u.py +++ b/src/syncify/local/playlist/m3u.py @@ -3,12 +3,12 @@ from dataclasses import dataclass from os.path import exists, dirname -from syncify.abstract.misc import Result -from syncify.local.playlist._match import LocalMatcher -from syncify.local.playlist._playlist import LocalPlaylist +from syncify.shared.core.misc import Result +from syncify.local.playlist.match import LocalMatcher +from syncify.local.playlist.base import LocalPlaylist from syncify.local.track import LocalTrack -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils import UnitCollection +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.types import UnitCollection @dataclass(frozen=True) diff --git a/src/syncify/local/playlist/_match.py b/src/syncify/local/playlist/match.py similarity index 98% rename from src/syncify/local/playlist/_match.py rename to src/syncify/local/playlist/match.py index f3094c31..17c1dbe0 100644 --- a/src/syncify/local/playlist/_match.py +++ b/src/syncify/local/playlist/match.py @@ -3,14 +3,14 @@ from os.path import exists from typing import Any, Self -from syncify.abstract.enums import Fields -from syncify.abstract.misc import Result +from syncify.shared.core.enums import Fields +from syncify.shared.core.misc import Result from syncify.local.track import LocalTrack from syncify.processors.base import MusicBeeProcessor from syncify.processors.compare import Comparer from syncify.processors.sort import ItemSorter -from syncify.utils import UnitSequence, UnitCollection -from syncify.utils.helpers import to_collection +from syncify.shared.types import UnitSequence, UnitCollection +from syncify.shared.utils import to_collection @dataclass(frozen=True) diff --git a/src/syncify/local/playlist/_utils.py b/src/syncify/local/playlist/utils.py similarity index 90% rename from src/syncify/local/playlist/_utils.py rename to src/syncify/local/playlist/utils.py index 42709c3b..692e4f3a 100644 --- a/src/syncify/local/playlist/_utils.py +++ b/src/syncify/local/playlist/utils.py @@ -2,12 +2,12 @@ from os.path import splitext from syncify.local.exception import InvalidFileType -from syncify.local.playlist._m3u import M3U -from syncify.local.playlist._playlist import LocalPlaylist -from syncify.local.playlist._xautopf import XAutoPF +from syncify.local.playlist.m3u import M3U +from syncify.local.playlist.base import LocalPlaylist +from syncify.local.playlist.xautopf import XAutoPF from syncify.local.track import LocalTrack -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils import UnitCollection +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.types import UnitCollection PLAYLIST_CLASSES = frozenset({M3U, XAutoPF}) PLAYLIST_FILETYPES = frozenset(filetype for c in PLAYLIST_CLASSES for filetype in c.valid_extensions) diff --git a/src/syncify/local/playlist/_xautopf.py b/src/syncify/local/playlist/xautopf.py similarity index 94% rename from src/syncify/local/playlist/_xautopf.py rename to src/syncify/local/playlist/xautopf.py index 1039c43a..317cf6d5 100644 --- a/src/syncify/local/playlist/_xautopf.py +++ b/src/syncify/local/playlist/xautopf.py @@ -6,15 +6,15 @@ import xmltodict -from syncify.abstract.enums import Fields -from syncify.abstract.misc import Result -from syncify.local.playlist._match import LocalMatcher -from syncify.local.playlist._playlist import LocalPlaylist +from syncify.shared.core.enums import Fields +from syncify.shared.core.misc import Result +from syncify.local.playlist.match import LocalMatcher +from syncify.local.playlist.base import LocalPlaylist from syncify.local.track import LocalTrack from syncify.processors.limit import ItemLimiter from syncify.processors.sort import ItemSorter -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils import UnitCollection +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.types import UnitCollection @dataclass(frozen=True) diff --git a/src/syncify/local/track/__init__.py b/src/syncify/local/track/__init__.py index 4bec18f8..8292b912 100644 --- a/src/syncify/local/track/__init__.py +++ b/src/syncify/local/track/__init__.py @@ -1,7 +1,7 @@ from ._base.track import LocalTrack from ._base.writer import SyncResultTrack -from ._flac import FLAC -from ._m4a import M4A -from ._mp3 import MP3 -from ._utils import TRACK_CLASSES, TRACK_FILETYPES, load_track -from ._wma import WMA +from .flac import FLAC +from .m4a import M4A +from .mp3 import MP3 +from .utils import TRACK_CLASSES, TRACK_FILETYPES, load_track +from .wma import WMA diff --git a/src/syncify/local/track/_base/reader.py b/src/syncify/local/track/_base/reader.py index 1d24c567..21438635 100644 --- a/src/syncify/local/track/_base/reader.py +++ b/src/syncify/local/track/_base/reader.py @@ -7,15 +7,15 @@ import mutagen from PIL import Image -from syncify.abstract.enums import TagMap -from syncify.abstract.object import Track -from syncify.exception import SyncifyValueError -from syncify.fields import LocalTrackField -from syncify.local import LocalItem -from syncify.remote.enums import RemoteIDType -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils import UnitIterable -from syncify.utils.helpers import to_collection +from syncify.shared.core.enums import TagMap +from syncify.shared.core.object import Track +from syncify.shared.exception import SyncifyValueError +from syncify.local.track.fields import LocalTrackField +from syncify.local.base import LocalItem +from syncify.shared.remote.enums import RemoteIDType +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.types import UnitIterable +from syncify.shared.utils import to_collection class TagReader(LocalItem, Track, metaclass=ABCMeta): diff --git a/src/syncify/local/track/_base/track.py b/src/syncify/local/track/_base/track.py index 63e284ea..dbb03f74 100644 --- a/src/syncify/local/track/_base/track.py +++ b/src/syncify/local/track/_base/track.py @@ -9,16 +9,16 @@ import mutagen -from syncify.abstract import Item -from syncify.abstract.object import Track -from syncify.exception import SyncifyKeyError, SyncifyAttributeError, SyncifyTypeError -from syncify.fields import TrackField -from syncify.local._file import File +from syncify.shared.core.base import Item +from syncify.shared.core.object import Track +from syncify.shared.exception import SyncifyKeyError, SyncifyAttributeError, SyncifyTypeError +from syncify.shared.fields import TrackField +from syncify.local.file import File from syncify.local.exception import FileDoesNotExistError from syncify.local.track._base.reader import TagReader from syncify.local.track._base.writer import TagWriter -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils import UnitIterable +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.types import UnitIterable class LocalTrack(TagWriter, metaclass=ABCMeta): diff --git a/src/syncify/local/track/_base/writer.py b/src/syncify/local/track/_base/writer.py index 51832711..ef71759b 100644 --- a/src/syncify/local/track/_base/writer.py +++ b/src/syncify/local/track/_base/writer.py @@ -6,11 +6,11 @@ import mutagen -from syncify.abstract.misc import Result -from syncify.fields import LocalTrackField as Tags +from syncify.shared.core.misc import Result +from syncify.local.track.fields import LocalTrackField as Tags from syncify.local.track._base.reader import TagReader -from syncify.utils import UnitIterable -from syncify.utils.helpers import to_collection +from syncify.shared.types import UnitIterable +from syncify.shared.utils import to_collection @dataclass(frozen=True) diff --git a/src/syncify/local/track/fields.py b/src/syncify/local/track/fields.py new file mode 100644 index 00000000..e8b87c4c --- /dev/null +++ b/src/syncify/local/track/fields.py @@ -0,0 +1,53 @@ +from syncify.shared.core.enums import TagFields +from syncify.shared.fields import TrackFieldMixin + + +class LocalTrackField(TrackFieldMixin): + """Represent all currently supported fields for objects of type :py:class:`LocalTrack`""" + ALL = TagFields.ALL.value + + TITLE = TagFields.TITLE.value + ARTIST = TagFields.ARTIST.value + ALBUM = TagFields.ALBUM.value + ALBUM_ARTIST = TagFields.ALBUM_ARTIST.value + TRACK = TagFields.TRACK_NUMBER.value + 500 + TRACK_NUMBER = TagFields.TRACK_NUMBER.value + TRACK_TOTAL = TagFields.TRACK_TOTAL.value + GENRES = TagFields.GENRES.value + DATE = TagFields.DATE.value + YEAR = TagFields.YEAR.value + MONTH = TagFields.MONTH.value + DAY = TagFields.DAY.value + BPM = TagFields.BPM.value + KEY = TagFields.KEY.value + DISC = TagFields.DISC_NUMBER.value + 500 + DISC_NUMBER = TagFields.DISC_NUMBER.value + DISC_TOTAL = TagFields.DISC_TOTAL.value + COMPILATION = TagFields.COMPILATION.value + COMMENTS = TagFields.COMMENTS.value + IMAGES = TagFields.IMAGES.value + LENGTH = TagFields.LENGTH.value + RATING = TagFields.RATING.value + + # file properties + PATH = TagFields.PATH.value + FOLDER = TagFields.FOLDER.value + FILENAME = TagFields.FILENAME.value + EXT = TagFields.EXT.value + SIZE = TagFields.SIZE.value + KIND = TagFields.KIND.value + CHANNELS = TagFields.CHANNELS.value + BIT_RATE = TagFields.BIT_RATE.value + BIT_DEPTH = TagFields.BIT_DEPTH.value + SAMPLE_RATE = TagFields.SAMPLE_RATE.value + + # date properties + DATE_MODIFIED = TagFields.DATE_MODIFIED.value + DATE_ADDED = TagFields.DATE_ADDED.value + LAST_PLAYED = TagFields.LAST_PLAYED.value + + # miscellaneous properties + PLAY_COUNT = TagFields.PLAY_COUNT.value + + # remote properties + URI = TagFields.URI.value diff --git a/src/syncify/local/track/_flac.py b/src/syncify/local/track/flac.py similarity index 95% rename from src/syncify/local/track/_flac.py rename to src/syncify/local/track/flac.py index 45499e56..105887a6 100644 --- a/src/syncify/local/track/_flac.py +++ b/src/syncify/local/track/flac.py @@ -7,10 +7,10 @@ import mutagen.id3 from PIL import Image -from syncify.abstract.enums import TagMap -from syncify.fields import LocalTrackField -from syncify.local import open_image, get_image_bytes -from syncify.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.core.enums import TagMap +from syncify.local.track.fields import LocalTrackField +from syncify.local.file import open_image, get_image_bytes +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler from ._base import LocalTrack diff --git a/src/syncify/local/track/_m4a.py b/src/syncify/local/track/m4a.py similarity index 97% rename from src/syncify/local/track/_m4a.py rename to src/syncify/local/track/m4a.py index 6502e987..34092072 100644 --- a/src/syncify/local/track/_m4a.py +++ b/src/syncify/local/track/m4a.py @@ -6,10 +6,10 @@ import mutagen.mp4 from PIL import Image -from syncify.abstract.enums import TagMap -from syncify.local import open_image, get_image_bytes -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils.helpers import to_collection +from syncify.shared.core.enums import TagMap +from syncify.local.file import open_image, get_image_bytes +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.utils import to_collection from ._base import LocalTrack diff --git a/src/syncify/local/track/_mp3.py b/src/syncify/local/track/mp3.py similarity index 97% rename from src/syncify/local/track/_mp3.py rename to src/syncify/local/track/mp3.py index c05f1e21..d8a14543 100644 --- a/src/syncify/local/track/_mp3.py +++ b/src/syncify/local/track/mp3.py @@ -7,10 +7,10 @@ import mutagen.mp3 from PIL import Image -from syncify.abstract.enums import TagMap -from syncify.fields import LocalTrackField -from syncify.local import open_image, get_image_bytes -from syncify.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.core.enums import TagMap +from syncify.local.track.fields import LocalTrackField +from syncify.local.file import open_image, get_image_bytes +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler from ._base import LocalTrack diff --git a/src/syncify/local/track/_utils.py b/src/syncify/local/track/utils.py similarity index 87% rename from src/syncify/local/track/_utils.py rename to src/syncify/local/track/utils.py index d566e38e..f21dde68 100644 --- a/src/syncify/local/track/_utils.py +++ b/src/syncify/local/track/utils.py @@ -3,11 +3,11 @@ from syncify.local.exception import InvalidFileType from syncify.local.track import LocalTrack -from syncify.local.track._flac import FLAC -from syncify.local.track._m4a import M4A -from syncify.local.track._mp3 import MP3 -from syncify.local.track._wma import WMA -from syncify.remote.processors.wrangle import RemoteDataWrangler +from syncify.local.track.flac import FLAC +from syncify.local.track.m4a import M4A +from syncify.local.track.mp3 import MP3 +from syncify.local.track.wma import WMA +from syncify.shared.remote.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/src/syncify/local/track/_wma.py b/src/syncify/local/track/wma.py similarity index 97% rename from src/syncify/local/track/_wma.py rename to src/syncify/local/track/wma.py index b4259ca1..4834475f 100644 --- a/src/syncify/local/track/_wma.py +++ b/src/syncify/local/track/wma.py @@ -8,9 +8,9 @@ import mutagen.id3 from PIL import Image, UnidentifiedImageError -from syncify.abstract.enums import TagMap -from syncify.local import open_image, get_image_bytes -from syncify.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.core.enums import TagMap +from syncify.local.file import open_image, get_image_bytes +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler from ._base import LocalTrack diff --git a/src/syncify/utils/printers.py b/src/syncify/printers.py similarity index 100% rename from src/syncify/utils/printers.py rename to src/syncify/printers.py diff --git a/src/syncify/processors/base.py b/src/syncify/processors/base.py index 66252666..9db7d456 100644 --- a/src/syncify/processors/base.py +++ b/src/syncify/processors/base.py @@ -3,7 +3,7 @@ from functools import partial, update_wrapper from typing import Any, Self, Optional -from syncify.abstract.misc import PrettyPrinter +from syncify.shared.core.misc import PrettyPrinter from syncify.processors.exception import ProcessorLookupError diff --git a/src/syncify/processors/compare.py b/src/syncify/processors/compare.py index 5d398c7f..5ca5245e 100644 --- a/src/syncify/processors/compare.py +++ b/src/syncify/processors/compare.py @@ -5,15 +5,15 @@ from operator import mul from typing import Any, Self -from syncify.abstract import Item -from syncify.abstract.enums import Field -from syncify.fields import LocalTrackField +from syncify.shared.core.base import Item +from syncify.shared.core.enums import Field +from syncify.local.track.fields import LocalTrackField from syncify.local.exception import FieldError from syncify.processors.base import DynamicProcessor, MusicBeeProcessor, dynamicprocessormethod from syncify.processors.exception import ItemComparerError from syncify.processors.time import TimeMapper -from syncify.utils import UnitSequence -from syncify.utils.helpers import to_collection +from syncify.shared.types import UnitSequence +from syncify.shared.utils import to_collection # Map of MusicBee field name to Field enum # noinspection SpellCheckingInspection diff --git a/src/syncify/processors/exception.py b/src/syncify/processors/exception.py index a378ddd3..9f689424 100644 --- a/src/syncify/processors/exception.py +++ b/src/syncify/processors/exception.py @@ -1,4 +1,4 @@ -from syncify.exception import SyncifyError +from syncify.shared.exception import SyncifyError class ProcessorError(SyncifyError): diff --git a/src/syncify/processors/limit.py b/src/syncify/processors/limit.py index 34ebcb9a..216114f6 100644 --- a/src/syncify/processors/limit.py +++ b/src/syncify/processors/limit.py @@ -4,10 +4,10 @@ from random import shuffle from typing import Any, Self -from syncify.abstract import Item -from syncify.abstract.enums import SyncifyEnum, Fields -from syncify.abstract.object import Track -from syncify.local import File +from syncify.shared.core.base import Item +from syncify.shared.core.enums import SyncifyEnum, Fields +from syncify.shared.core.object import Track +from syncify.local.file import File from syncify.processors.base import DynamicProcessor, MusicBeeProcessor, dynamicprocessormethod from syncify.processors.exception import ItemLimiterError from syncify.processors.sort import ItemSorter diff --git a/src/syncify/processors/match.py b/src/syncify/processors/match.py index 2bbb3155..aae7a56b 100644 --- a/src/syncify/processors/match.py +++ b/src/syncify/processors/match.py @@ -5,15 +5,15 @@ from dataclasses import dataclass from typing import Any -from syncify.abstract import NamedObject -from syncify.abstract.collection import ItemCollection -from syncify.abstract.enums import TagField, TagFields as Tag, ALL_TAG_FIELDS -from syncify.abstract.misc import PrettyPrinter -from syncify.abstract.object import Track, Album +from syncify.shared.core.base import NamedObject +from syncify.shared.core.collection import ItemCollection +from syncify.shared.core.enums import TagField, TagFields as Tag, ALL_TAG_FIELDS +from syncify.shared.core.misc import PrettyPrinter +from syncify.shared.core.object import Track, Album from syncify.processors.base import ItemProcessor -from syncify.utils import UnitIterable -from syncify.utils.helpers import limit_value, to_collection -from syncify.utils.logger import SyncifyLogger +from syncify.shared.types import UnitIterable +from syncify.shared.utils import limit_value, to_collection +from syncify.shared.logger import SyncifyLogger @dataclass diff --git a/src/syncify/processors/sort.py b/src/syncify/processors/sort.py index 39cba9c1..345dc136 100644 --- a/src/syncify/processors/sort.py +++ b/src/syncify/processors/sort.py @@ -4,12 +4,12 @@ from random import shuffle from typing import Any, Self -from syncify.abstract import Item -from syncify.abstract.enums import SyncifyEnum, Field, Fields +from syncify.shared.core.base import Item +from syncify.shared.core.enums import SyncifyEnum, Field, Fields from syncify.local.exception import FieldError from syncify.processors.base import MusicBeeProcessor -from syncify.utils import UnitSequence, UnitIterable -from syncify.utils.helpers import flatten_nested, strip_ignore_words, to_collection, limit_value +from syncify.shared.types import UnitSequence, UnitIterable +from syncify.shared.utils import flatten_nested, strip_ignore_words, to_collection, limit_value def _get_field_from_code(field_code: int) -> Field | None: diff --git a/src/syncify/processors/time.py b/src/syncify/processors/time.py index a74d5666..9db73bac 100644 --- a/src/syncify/processors/time.py +++ b/src/syncify/processors/time.py @@ -3,7 +3,7 @@ from dateutil.relativedelta import relativedelta -from syncify.abstract.misc import PrettyPrinter +from syncify.shared.core.misc import PrettyPrinter from syncify.processors.base import DynamicProcessor, dynamicprocessormethod diff --git a/src/syncify/report.py b/src/syncify/report.py index e014bc6c..b201d722 100644 --- a/src/syncify/report.py +++ b/src/syncify/report.py @@ -1,13 +1,14 @@ import logging from collections.abc import Iterable -from syncify.abstract import Item -from syncify.abstract.collection import ItemCollection -from syncify.abstract.enums import TagField, Fields, ALL_FIELDS, TagFields -from syncify.abstract.object import Library, Playlist +from syncify.shared.core.base import Item +from syncify.shared.core.collection import ItemCollection +from syncify.shared.core.enums import TagField, Fields, ALL_FIELDS, TagFields +from syncify.shared.core.object import Library, Playlist from syncify.local.library import LocalLibrary -from syncify.utils.helpers import align_and_truncate, get_max_width, UnitIterable, to_collection -from syncify.utils.logger import SyncifyLogger, REPORT +from syncify.shared.utils import align_and_truncate, get_max_width, to_collection +from syncify.shared.types import UnitIterable +from syncify.shared.logger import SyncifyLogger, REPORT def report_playlist_differences( diff --git a/src/syncify/remote/processors/__init__.py b/src/syncify/shared/__init__.py similarity index 100% rename from src/syncify/remote/processors/__init__.py rename to src/syncify/shared/__init__.py diff --git a/src/syncify/shared/api/__init__.py b/src/syncify/shared/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/syncify/api/_authorise.py b/src/syncify/shared/api/authorise.py similarity index 97% rename from src/syncify/api/_authorise.py rename to src/syncify/shared/api/authorise.py index a3abb778..ad795b3d 100644 --- a/src/syncify/api/_authorise.py +++ b/src/syncify/shared/api/authorise.py @@ -11,8 +11,8 @@ import requests from syncify import PROGRAM_NAME -from syncify.api.exception import APIError -from syncify.utils.logger import SyncifyLogger +from syncify.shared.api.exception import APIError +from syncify.shared.logger import SyncifyLogger class APIAuthoriser: diff --git a/src/syncify/api/exception.py b/src/syncify/shared/api/exception.py similarity index 89% rename from src/syncify/api/exception.py rename to src/syncify/shared/api/exception.py index 3ac6bba4..35fce37f 100644 --- a/src/syncify/api/exception.py +++ b/src/syncify/shared/api/exception.py @@ -1,6 +1,6 @@ from requests import Response -from syncify.exception import SyncifyError +from syncify.shared.exception import SyncifyError class APIError(SyncifyError): diff --git a/src/syncify/api/_request.py b/src/syncify/shared/api/request.py similarity index 96% rename from src/syncify/api/_request.py rename to src/syncify/shared/api/request.py index 93f39361..5656a558 100644 --- a/src/syncify/api/_request.py +++ b/src/syncify/shared/api/request.py @@ -9,8 +9,8 @@ from requests import Response, Session from requests_cache import CachedSession -from syncify.api._authorise import APIAuthoriser -from syncify.api.exception import APIError +from syncify.shared.api.authorise import APIAuthoriser +from syncify.shared.api.exception import APIError class RequestHandler(APIAuthoriser): diff --git a/src/syncify/shared/core/__init__.py b/src/syncify/shared/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/syncify/abstract/_base.py b/src/syncify/shared/core/base.py similarity index 92% rename from src/syncify/abstract/_base.py rename to src/syncify/shared/core/base.py index a23a5a11..5ba71614 100644 --- a/src/syncify/abstract/_base.py +++ b/src/syncify/shared/core/base.py @@ -4,8 +4,8 @@ from collections.abc import Hashable from typing import Any -from syncify.abstract.enums import TagField -from syncify.abstract.misc import PrettyPrinter +from syncify.shared.core.enums import TagField +from syncify.shared.core.misc import PrettyPrinter class NamedObject(ABC): diff --git a/src/syncify/abstract/collection.py b/src/syncify/shared/core/collection.py similarity index 94% rename from src/syncify/abstract/collection.py rename to src/syncify/shared/core/collection.py index 4a14b9e8..830e9811 100644 --- a/src/syncify/abstract/collection.py +++ b/src/syncify/shared/core/collection.py @@ -4,11 +4,11 @@ from collections.abc import MutableSequence, Iterable, Mapping from typing import Any, SupportsIndex, Self -from syncify.abstract._base import Item, NamedObjectPrinter -from syncify.abstract.enums import Field -from syncify.exception import SyncifyTypeError, SyncifyKeyError +from syncify.shared.core.base import Item, NamedObjectPrinter +from syncify.shared.core.enums import Field +from syncify.shared.exception import SyncifyTypeError, SyncifyKeyError from syncify.processors.sort import ShuffleMode, ShuffleBy, ItemSorter -from syncify.utils import UnitSequence +from syncify.shared.types import UnitSequence class ItemCollection[T: Item](NamedObjectPrinter, MutableSequence[T], metaclass=ABCMeta): diff --git a/src/syncify/abstract/enums.py b/src/syncify/shared/core/enums.py similarity index 95% rename from src/syncify/abstract/enums.py rename to src/syncify/shared/core/enums.py index 6beb60be..f2cbe2e5 100644 --- a/src/syncify/abstract/enums.py +++ b/src/syncify/shared/core/enums.py @@ -3,9 +3,9 @@ from enum import IntEnum from typing import Self -from syncify.exception import SyncifyEnumError -from syncify.utils import UnitIterable -from syncify.utils.helpers import unique_list +from syncify.shared.exception import SyncifyEnumError +from syncify.shared.types import UnitIterable +from syncify.shared.utils import unique_list class SyncifyEnum(IntEnum): diff --git a/src/syncify/abstract/misc.py b/src/syncify/shared/core/misc.py similarity index 97% rename from src/syncify/abstract/misc.py rename to src/syncify/shared/core/misc.py index 5f385b69..3e68934e 100644 --- a/src/syncify/abstract/misc.py +++ b/src/syncify/shared/core/misc.py @@ -5,7 +5,7 @@ from datetime import datetime, date from typing import Any -from syncify.utils.helpers import to_collection +from syncify.shared.utils import to_collection _T_JSON_VALUE = str | int | float | list | dict | bool | None diff --git a/src/syncify/abstract/object.py b/src/syncify/shared/core/object.py similarity index 95% rename from src/syncify/abstract/object.py rename to src/syncify/shared/core/object.py index 35fedc26..4fa08625 100644 --- a/src/syncify/abstract/object.py +++ b/src/syncify/shared/core/object.py @@ -7,12 +7,12 @@ from copy import deepcopy from typing import Any, Self -from syncify.abstract._base import Item -from syncify.abstract.collection import ItemCollection -from syncify.abstract.misc import Filter -from syncify.exception import SyncifyKeyError, SyncifyTypeError -from syncify.utils.helpers import to_collection, align_and_truncate, get_max_width -from syncify.utils.logger import SyncifyLogger +from syncify.shared.core.base import Item +from syncify.shared.core.collection import ItemCollection +from syncify.shared.core.misc import Filter +from syncify.shared.exception import SyncifyKeyError, SyncifyTypeError +from syncify.shared.utils import to_collection, align_and_truncate, get_max_width +from syncify.shared.logger import SyncifyLogger class Track(Item, metaclass=ABCMeta): diff --git a/src/syncify/exception.py b/src/syncify/shared/exception.py similarity index 94% rename from src/syncify/exception.py rename to src/syncify/shared/exception.py index bbfdbe1a..3030767b 100644 --- a/src/syncify/exception.py +++ b/src/syncify/shared/exception.py @@ -1,4 +1,5 @@ -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any class SafeDict(dict): diff --git a/src/syncify/fields.py b/src/syncify/shared/fields.py similarity index 64% rename from src/syncify/fields.py rename to src/syncify/shared/fields.py index 2843c939..33b176b7 100644 --- a/src/syncify/fields.py +++ b/src/syncify/shared/fields.py @@ -1,6 +1,6 @@ from typing import Self -from syncify.abstract.enums import Field, Fields, TagField, TagFields +from syncify.shared.core.enums import Field, Fields, TagField, TagFields class TrackFieldMixin(TagField): @@ -54,57 +54,6 @@ class TrackField(TrackFieldMixin): URI = TagFields.URI.value -class LocalTrackField(TrackFieldMixin): - """Represent all currently supported fields for objects of type :py:class:`LocalTrack`""" - ALL = TagFields.ALL.value - - TITLE = TagFields.TITLE.value - ARTIST = TagFields.ARTIST.value - ALBUM = TagFields.ALBUM.value - ALBUM_ARTIST = TagFields.ALBUM_ARTIST.value - TRACK = TagFields.TRACK_NUMBER.value + 500 - TRACK_NUMBER = TagFields.TRACK_NUMBER.value - TRACK_TOTAL = TagFields.TRACK_TOTAL.value - GENRES = TagFields.GENRES.value - DATE = TagFields.DATE.value - YEAR = TagFields.YEAR.value - MONTH = TagFields.MONTH.value - DAY = TagFields.DAY.value - BPM = TagFields.BPM.value - KEY = TagFields.KEY.value - DISC = TagFields.DISC_NUMBER.value + 500 - DISC_NUMBER = TagFields.DISC_NUMBER.value - DISC_TOTAL = TagFields.DISC_TOTAL.value - COMPILATION = TagFields.COMPILATION.value - COMMENTS = TagFields.COMMENTS.value - IMAGES = TagFields.IMAGES.value - LENGTH = TagFields.LENGTH.value - RATING = TagFields.RATING.value - - # file properties - PATH = TagFields.PATH.value - FOLDER = TagFields.FOLDER.value - FILENAME = TagFields.FILENAME.value - EXT = TagFields.EXT.value - SIZE = TagFields.SIZE.value - KIND = TagFields.KIND.value - CHANNELS = TagFields.CHANNELS.value - BIT_RATE = TagFields.BIT_RATE.value - BIT_DEPTH = TagFields.BIT_DEPTH.value - SAMPLE_RATE = TagFields.SAMPLE_RATE.value - - # date properties - DATE_MODIFIED = TagFields.DATE_MODIFIED.value - DATE_ADDED = TagFields.DATE_ADDED.value - LAST_PLAYED = TagFields.LAST_PLAYED.value - - # miscellaneous properties - PLAY_COUNT = TagFields.PLAY_COUNT.value - - # remote properties - URI = TagFields.URI.value - - class PlaylistField(Field): ALL = Fields.ALL.value diff --git a/src/syncify/utils/logger.py b/src/syncify/shared/logger.py similarity index 96% rename from src/syncify/utils/logger.py rename to src/syncify/shared/logger.py index 11f37a60..6097890f 100644 --- a/src/syncify/utils/logger.py +++ b/src/syncify/shared/logger.py @@ -39,6 +39,7 @@ class SyncifyLogger(logging.Logger): """The logger for all logging operations in Syncify.""" compact: bool = False + disable_bars: bool = False _bars: list[tqdm] = [] @property @@ -115,7 +116,7 @@ def get_progress_bar[T: Any]( iterable=iterable, total=total, leave=leave, - disable=kwargs.get("disable", False), + disable=self.disable_bars or kwargs.get("disable", False), file=sys.stdout, ncols=cols, colour=kwargs.get("colour", "blue"), diff --git a/src/syncify/remote/__init__.py b/src/syncify/shared/remote/__init__.py similarity index 100% rename from src/syncify/remote/__init__.py rename to src/syncify/shared/remote/__init__.py diff --git a/src/syncify/remote/_base.py b/src/syncify/shared/remote/_base.py similarity index 100% rename from src/syncify/remote/_base.py rename to src/syncify/shared/remote/_base.py diff --git a/src/syncify/remote/api.py b/src/syncify/shared/remote/api.py similarity index 95% rename from src/syncify/remote/api.py rename to src/syncify/shared/remote/api.py index 714d8e81..73c61589 100644 --- a/src/syncify/remote/api.py +++ b/src/syncify/shared/remote/api.py @@ -3,12 +3,12 @@ from collections.abc import Collection, MutableMapping, Mapping from typing import Any, Self -from syncify.api import RequestHandler -from syncify.remote.enums import RemoteIDType, RemoteObjectType -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.remote.types import APIMethodInputType -from syncify.utils.helpers import align_and_truncate -from syncify.utils.logger import SyncifyLogger +from syncify.shared.api.request import RequestHandler +from syncify.shared.remote.enums import RemoteIDType, RemoteObjectType +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.remote.types import APIMethodInputType +from syncify.shared.utils import align_and_truncate +from syncify.shared.logger import SyncifyLogger class RemoteAPI(RemoteDataWrangler, metaclass=ABCMeta): diff --git a/src/syncify/remote/base.py b/src/syncify/shared/remote/base.py similarity index 92% rename from src/syncify/remote/base.py rename to src/syncify/shared/remote/base.py index fd4805bc..61c0977e 100644 --- a/src/syncify/remote/base.py +++ b/src/syncify/shared/remote/base.py @@ -1,10 +1,10 @@ from abc import ABCMeta, abstractmethod from typing import Any, Self -from syncify.abstract import Item, NamedObjectPrinter -from syncify.api.exception import APIError -from syncify.remote.api import RemoteAPI -from syncify.remote._base import Remote +from syncify.shared.core.base import Item, NamedObjectPrinter +from syncify.shared.api.exception import APIError +from syncify.shared.remote.api import RemoteAPI +from syncify.shared.remote._base import Remote class RemoteObject(NamedObjectPrinter, Remote, metaclass=ABCMeta): diff --git a/src/syncify/remote/config.py b/src/syncify/shared/remote/config.py similarity index 70% rename from src/syncify/remote/config.py rename to src/syncify/shared/remote/config.py index d0aa0189..24932a11 100644 --- a/src/syncify/remote/config.py +++ b/src/syncify/shared/remote/config.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from syncify.remote.object import RemoteTrack, RemoteAlbum, RemotePlaylist, RemoteArtist +from syncify.shared.remote.object import RemoteTrack, RemoteAlbum, RemotePlaylist, RemoteArtist @dataclass diff --git a/src/syncify/remote/enums.py b/src/syncify/shared/remote/enums.py similarity index 86% rename from src/syncify/remote/enums.py rename to src/syncify/shared/remote/enums.py index e765db07..c769535d 100644 --- a/src/syncify/remote/enums.py +++ b/src/syncify/shared/remote/enums.py @@ -1,4 +1,4 @@ -from syncify.abstract.enums import SyncifyEnum +from syncify.shared.core.enums import SyncifyEnum class RemoteIDType(SyncifyEnum): diff --git a/src/syncify/remote/exception.py b/src/syncify/shared/remote/exception.py similarity index 89% rename from src/syncify/remote/exception.py rename to src/syncify/shared/remote/exception.py index a82d9af4..4af5468a 100644 --- a/src/syncify/remote/exception.py +++ b/src/syncify/shared/remote/exception.py @@ -1,7 +1,7 @@ from typing import Any -from syncify.exception import SyncifyError -from syncify.remote.enums import RemoteIDType, RemoteObjectType +from syncify.shared.exception import SyncifyError +from syncify.shared.remote.enums import RemoteIDType, RemoteObjectType class RemoteError(SyncifyError): diff --git a/src/syncify/remote/library.py b/src/syncify/shared/remote/library.py similarity index 97% rename from src/syncify/remote/library.py rename to src/syncify/shared/remote/library.py index 146c6021..e9b3d4d8 100644 --- a/src/syncify/remote/library.py +++ b/src/syncify/shared/remote/library.py @@ -3,15 +3,15 @@ from functools import partial from typing import Any, Literal -from syncify.abstract import Item -from syncify.abstract.object import Track, Library, Playlist -from syncify.remote.api import RemoteAPI -from syncify.remote.config import RemoteObjectClasses -from syncify.remote.enums import RemoteObjectType -from syncify.remote.object import RemoteTrack, RemoteCollection, RemotePlaylist, SyncResultRemotePlaylist, \ +from syncify.shared.core.base import Item +from syncify.shared.core.object import Track, Library, Playlist +from syncify.shared.remote.api import RemoteAPI +from syncify.shared.remote.config import RemoteObjectClasses +from syncify.shared.remote.enums import RemoteObjectType +from syncify.shared.remote.object import RemoteTrack, RemoteCollection, RemotePlaylist, SyncResultRemotePlaylist, \ RemoteArtist, RemoteAlbum -from syncify.utils.helpers import align_and_truncate, get_max_width -from syncify.utils.logger import REPORT, STAT +from syncify.shared.utils import align_and_truncate, get_max_width +from syncify.shared.logger import REPORT, STAT class RemoteLibrary[T: RemoteTrack](Library[T], RemoteCollection[T], metaclass=ABCMeta): diff --git a/src/syncify/remote/object.py b/src/syncify/shared/remote/object.py similarity index 95% rename from src/syncify/remote/object.py rename to src/syncify/shared/remote/object.py index a1e00ce4..644b2ce3 100644 --- a/src/syncify/remote/object.py +++ b/src/syncify/shared/remote/object.py @@ -6,18 +6,18 @@ from datetime import datetime from typing import Self, Literal, Any -from syncify.abstract import Item -from syncify.abstract.collection import ItemCollection -from syncify.abstract.misc import Result -from syncify.abstract.object import Track, Album, Playlist, Artist -from syncify.api.exception import APIError -from syncify.exception import SyncifyKeyError -from syncify.remote.api import RemoteAPI -from syncify.remote.enums import RemoteIDType -from syncify.remote.exception import RemoteIDTypeError, RemoteError -from syncify.remote.base import RemoteObject, RemoteItem -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils.helpers import get_most_common_values +from syncify.shared.core.base import Item +from syncify.shared.core.collection import ItemCollection +from syncify.shared.core.misc import Result +from syncify.shared.core.object import Track, Album, Playlist, Artist +from syncify.shared.api.exception import APIError +from syncify.shared.exception import SyncifyKeyError +from syncify.shared.remote.api import RemoteAPI +from syncify.shared.remote.enums import RemoteIDType +from syncify.shared.remote.exception import RemoteIDTypeError, RemoteError +from syncify.shared.remote.base import RemoteObject, RemoteItem +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.utils import get_most_common_values class RemoteItemWranglerMixin[T: RemoteObject](RemoteItem, RemoteDataWrangler, metaclass=ABCMeta): diff --git a/src/syncify/shared/remote/processors/__init__.py b/src/syncify/shared/remote/processors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/syncify/remote/processors/check.py b/src/syncify/shared/remote/processors/check.py similarity index 94% rename from src/syncify/remote/processors/check.py rename to src/syncify/shared/remote/processors/check.py index a6930093..8ce23553 100644 --- a/src/syncify/remote/processors/check.py +++ b/src/syncify/shared/remote/processors/check.py @@ -5,19 +5,19 @@ from dataclasses import dataclass, field from syncify import PROGRAM_NAME -from syncify.abstract import Item -from syncify.abstract.collection import ItemCollection -from syncify.abstract.enums import Fields -from syncify.abstract.misc import Result -from syncify.abstract.object import Track +from syncify.shared.core.base import Item +from syncify.shared.core.collection import ItemCollection +from syncify.shared.core.enums import Fields +from syncify.shared.core.misc import Result +from syncify.shared.core.object import Track from syncify.processors.match import ItemMatcher -from syncify.remote.api import RemoteAPI -from syncify.remote.config import RemoteObjectClasses -from syncify.remote.enums import RemoteObjectType, RemoteIDType -from syncify.remote.processors.search import RemoteItemSearcher -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils.helpers import get_user_input, get_max_width, align_and_truncate -from syncify.utils.logger import REPORT +from syncify.shared.remote.api import RemoteAPI +from syncify.shared.remote.config import RemoteObjectClasses +from syncify.shared.remote.enums import RemoteObjectType, RemoteIDType +from syncify.shared.remote.processors.search import RemoteItemSearcher +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.utils import get_user_input, get_max_width, align_and_truncate +from syncify.shared.logger import REPORT ALLOW_KARAOKE_DEFAULT = RemoteItemSearcher.settings_items.allow_karaoke diff --git a/src/syncify/remote/processors/search.py b/src/syncify/shared/remote/processors/search.py similarity index 93% rename from src/syncify/remote/processors/search.py rename to src/syncify/shared/remote/processors/search.py index 1d0e7602..0d723bdb 100644 --- a/src/syncify/remote/processors/search.py +++ b/src/syncify/shared/remote/processors/search.py @@ -4,18 +4,18 @@ from functools import partial from typing import Any -from syncify.abstract import Item, NamedObject -from syncify.abstract.collection import ItemCollection -from syncify.abstract.enums import TagField, TagFields as Tag -from syncify.abstract.misc import Result -from syncify.abstract.object import Track +from syncify.shared.core.base import Item, NamedObject +from syncify.shared.core.collection import ItemCollection +from syncify.shared.core.enums import TagField, TagFields as Tag +from syncify.shared.core.misc import Result +from syncify.shared.core.object import Track from syncify.processors.match import ItemMatcher -from syncify.remote.api import RemoteAPI -from syncify.remote._base import Remote -from syncify.remote.config import RemoteObjectClasses -from syncify.remote.enums import RemoteObjectType -from syncify.utils.helpers import align_and_truncate, get_max_width -from syncify.utils.logger import REPORT +from syncify.shared.remote.api import RemoteAPI +from syncify.shared.remote._base import Remote +from syncify.shared.remote.config import RemoteObjectClasses +from syncify.shared.remote.enums import RemoteObjectType +from syncify.shared.utils import align_and_truncate, get_max_width +from syncify.shared.logger import REPORT @dataclass(frozen=True) diff --git a/src/syncify/remote/processors/wrangle.py b/src/syncify/shared/remote/processors/wrangle.py similarity index 95% rename from src/syncify/remote/processors/wrangle.py rename to src/syncify/shared/remote/processors/wrangle.py index 00cdd6bb..9847166c 100644 --- a/src/syncify/remote/processors/wrangle.py +++ b/src/syncify/shared/remote/processors/wrangle.py @@ -2,10 +2,10 @@ from collections.abc import Mapping from typing import Any -from syncify.remote._base import Remote -from syncify.remote.enums import RemoteIDType, RemoteObjectType -from syncify.remote.exception import RemoteObjectTypeError -from syncify.remote.types import APIMethodInputType +from syncify.shared.remote._base import Remote +from syncify.shared.remote.enums import RemoteIDType, RemoteObjectType +from syncify.shared.remote.exception import RemoteObjectTypeError +from syncify.shared.remote.types import APIMethodInputType class RemoteDataWrangler(Remote, metaclass=ABCMeta): diff --git a/src/syncify/remote/types.py b/src/syncify/shared/remote/types.py similarity index 73% rename from src/syncify/remote/types.py rename to src/syncify/shared/remote/types.py index 2261896b..531c8bcb 100644 --- a/src/syncify/remote/types.py +++ b/src/syncify/shared/remote/types.py @@ -1,7 +1,7 @@ from collections.abc import MutableMapping from typing import Any -from syncify.utils import UnitMutableSequence +from syncify.shared.types import UnitMutableSequence APIMethodInputType = UnitMutableSequence[str] | UnitMutableSequence[MutableMapping[str, Any]] diff --git a/src/syncify/shared/types.py b/src/syncify/shared/types.py new file mode 100644 index 00000000..b81eb4b9 --- /dev/null +++ b/src/syncify/shared/types.py @@ -0,0 +1,10 @@ +from collections.abc import Iterable, Sequence, MutableSequence, Collection +from typing import TypeVar + +UT = TypeVar('UT') +UnitIterable = UT | Iterable[UT] +UnitSequence = UT | Sequence[UT] +UnitMutableSequence = UT | MutableSequence[UT] +UnitCollection = UT | Collection[UT] + +Number = int | float diff --git a/src/syncify/utils/helpers.py b/src/syncify/shared/utils.py similarity index 92% rename from src/syncify/utils/helpers.py rename to src/syncify/shared/utils.py index daa67195..88597b89 100644 --- a/src/syncify/utils/helpers.py +++ b/src/syncify/shared/utils.py @@ -1,18 +1,11 @@ import re from collections import Counter -from collections.abc import Iterable, Collection, Sequence, MutableSequence, Mapping, MutableMapping +from collections.abc import Iterable, Collection, MutableSequence, Mapping, MutableMapping from os.path import sep -from typing import Any, TypeVar +from typing import Any -from syncify.exception import SyncifyTypeError, SafeDict - -UT = TypeVar('UT') -UnitIterable = UT | Iterable[UT] -UnitSequence = UT | Sequence[UT] -UnitMutableSequence = UT | MutableSequence[UT] -UnitCollection = UT | Collection[UT] - -Number = int | float +from syncify.shared.exception import SyncifyTypeError, SafeDict +from syncify.shared.types import Number ########################################################################### diff --git a/src/syncify/spotify/_base.py b/src/syncify/spotify/_base.py index 2f927056..853a5be2 100644 --- a/src/syncify/spotify/_base.py +++ b/src/syncify/spotify/_base.py @@ -1,4 +1,4 @@ -from syncify.remote import Remote +from syncify.shared.remote import Remote class SpotifyRemote(Remote): diff --git a/src/syncify/spotify/api/__init__.py b/src/syncify/spotify/api/__init__.py index 99832f27..84de7641 100644 --- a/src/syncify/spotify/api/__init__.py +++ b/src/syncify/spotify/api/__init__.py @@ -1 +1 @@ -from ._api import SpotifyAPI +from .api import SpotifyAPI diff --git a/src/syncify/spotify/api/_api.py b/src/syncify/spotify/api/api.py similarity index 89% rename from src/syncify/spotify/api/_api.py rename to src/syncify/spotify/api/api.py index 7ae54d83..6ce070a7 100644 --- a/src/syncify/spotify/api/_api.py +++ b/src/syncify/spotify/api/api.py @@ -4,11 +4,11 @@ from syncify import PROGRAM_NAME from syncify.spotify import URL_API, URL_AUTH -from syncify.spotify.api._core import SpotifyAPICore -from syncify.spotify.api._item import SpotifyAPIItems -from syncify.spotify.api._playlist import SpotifyAPIPlaylists +from syncify.spotify.api.misc import SpotifyAPIMisc +from syncify.spotify.api.item import SpotifyAPIItems +from syncify.spotify.api.playlist import SpotifyAPIPlaylists from syncify.spotify.processors.wrangle import SpotifyDataWrangler -from syncify.utils.helpers import safe_format_map +from syncify.shared.utils import safe_format_map # user authenticated access with scopes SPOTIFY_API_AUTH_ARGS = { @@ -54,7 +54,7 @@ } -class SpotifyAPI(SpotifyAPICore, SpotifyAPIItems, SpotifyAPIPlaylists, SpotifyDataWrangler): +class SpotifyAPI(SpotifyAPIMisc, SpotifyAPIItems, SpotifyAPIPlaylists, SpotifyDataWrangler): """ Collection of endpoints for the Spotify API. See :py:class:`RequestHandler` for more info on optional params to pass as ``**kwargs``. diff --git a/src/syncify/spotify/api/_base.py b/src/syncify/spotify/api/base.py similarity index 90% rename from src/syncify/spotify/api/_base.py rename to src/syncify/spotify/api/base.py index 7e44ce84..704267e0 100644 --- a/src/syncify/spotify/api/_base.py +++ b/src/syncify/spotify/api/base.py @@ -2,7 +2,7 @@ from typing import Any from urllib.parse import parse_qs, urlparse, urlencode, quote, urlunparse -from syncify.remote.api import RemoteAPI +from syncify.shared.remote.api import RemoteAPI class SpotifyAPIBase(RemoteAPI, metaclass=ABCMeta): diff --git a/src/syncify/spotify/api/_item.py b/src/syncify/spotify/api/item.py similarity index 96% rename from src/syncify/spotify/api/_item.py rename to src/syncify/spotify/api/item.py index 20453dd0..b15b5159 100644 --- a/src/syncify/spotify/api/_item.py +++ b/src/syncify/spotify/api/item.py @@ -5,12 +5,12 @@ from typing import Any from urllib.parse import parse_qs, urlparse -from syncify.api.exception import APIError -from syncify.remote.api import APIMethodInputType -from syncify.remote.enums import RemoteObjectType, RemoteIDType -from syncify.remote.exception import RemoteObjectTypeError -from syncify.spotify.api._base import SpotifyAPIBase -from syncify.utils.helpers import limit_value +from syncify.shared.api.exception import APIError +from syncify.shared.remote.api import APIMethodInputType +from syncify.shared.remote.enums import RemoteObjectType, RemoteIDType +from syncify.shared.remote.exception import RemoteObjectTypeError +from syncify.spotify.api.base import SpotifyAPIBase +from syncify.shared.utils import limit_value class SpotifyAPIItems(SpotifyAPIBase, metaclass=ABCMeta): diff --git a/src/syncify/spotify/api/_core.py b/src/syncify/spotify/api/misc.py similarity index 93% rename from src/syncify/spotify/api/_core.py rename to src/syncify/spotify/api/misc.py index fb724f6f..f95089d5 100644 --- a/src/syncify/spotify/api/_core.py +++ b/src/syncify/spotify/api/misc.py @@ -2,12 +2,12 @@ from collections.abc import MutableMapping from typing import Any -from syncify.remote.enums import RemoteIDType, RemoteObjectType -from syncify.spotify.api._base import SpotifyAPIBase -from syncify.utils.helpers import limit_value +from syncify.shared.remote.enums import RemoteIDType, RemoteObjectType +from syncify.spotify.api.base import SpotifyAPIBase +from syncify.shared.utils import limit_value -class SpotifyAPICore(SpotifyAPIBase, metaclass=ABCMeta): +class SpotifyAPIMisc(SpotifyAPIBase, metaclass=ABCMeta): def print_collection( self, diff --git a/src/syncify/spotify/api/_playlist.py b/src/syncify/spotify/api/playlist.py similarity index 95% rename from src/syncify/spotify/api/_playlist.py rename to src/syncify/spotify/api/playlist.py index aecdd086..192f581d 100644 --- a/src/syncify/spotify/api/_playlist.py +++ b/src/syncify/spotify/api/playlist.py @@ -4,10 +4,10 @@ from typing import Any from syncify import PROGRAM_NAME, PROGRAM_URL -from syncify.remote.enums import RemoteIDType, RemoteObjectType -from syncify.remote.exception import RemoteIDTypeError -from syncify.spotify.api._base import SpotifyAPIBase -from syncify.utils.helpers import limit_value +from syncify.shared.remote.enums import RemoteIDType, RemoteObjectType +from syncify.shared.remote.exception import RemoteIDTypeError +from syncify.spotify.api.base import SpotifyAPIBase +from syncify.shared.utils import limit_value class SpotifyAPIPlaylists(SpotifyAPIBase, metaclass=ABCMeta): diff --git a/src/syncify/spotify/base.py b/src/syncify/spotify/base.py index aad81cb3..3c4a5bf7 100644 --- a/src/syncify/spotify/base.py +++ b/src/syncify/spotify/base.py @@ -1,10 +1,10 @@ from abc import ABCMeta from typing import Any -from syncify.abstract.misc import PrettyPrinter -from syncify.remote.enums import RemoteObjectType -from syncify.remote.exception import RemoteObjectTypeError, RemoteError -from syncify.remote.base import RemoteObject, RemoteItem +from syncify.shared.core.misc import PrettyPrinter +from syncify.shared.remote.enums import RemoteObjectType +from syncify.shared.remote.exception import RemoteObjectTypeError, RemoteError +from syncify.shared.remote.base import RemoteObject, RemoteItem from syncify.spotify import SpotifyRemote from syncify.spotify.api import SpotifyAPI diff --git a/src/syncify/spotify/config.py b/src/syncify/spotify/config.py index 4f510238..399335a3 100644 --- a/src/syncify/spotify/config.py +++ b/src/syncify/spotify/config.py @@ -1,4 +1,4 @@ -from syncify.remote.config import RemoteObjectClasses +from syncify.shared.remote.config import RemoteObjectClasses from syncify.spotify.object import SpotifyPlaylist, SpotifyTrack, SpotifyAlbum, SpotifyArtist SPOTIFY_OBJECT_CLASSES = RemoteObjectClasses( diff --git a/src/syncify/spotify/exception.py b/src/syncify/spotify/exception.py index c0ade4b7..1b7a8386 100644 --- a/src/syncify/spotify/exception.py +++ b/src/syncify/spotify/exception.py @@ -1,4 +1,4 @@ -from syncify.remote.exception import RemoteError +from syncify.shared.remote.exception import RemoteError class SpotifyError(RemoteError): diff --git a/src/syncify/spotify/library.py b/src/syncify/spotify/library.py index e9746542..240b4ca6 100644 --- a/src/syncify/spotify/library.py +++ b/src/syncify/spotify/library.py @@ -1,11 +1,11 @@ from collections.abc import Collection, Mapping, Iterable from typing import Any -from syncify.abstract.misc import Filter -from syncify.abstract.object import Playlist, Library -from syncify.remote.config import RemoteObjectClasses -from syncify.remote.enums import RemoteObjectType -from syncify.remote.library import RemoteLibrary +from syncify.shared.core.misc import Filter +from syncify.shared.core.object import Playlist, Library +from syncify.shared.remote.config import RemoteObjectClasses +from syncify.shared.remote.enums import RemoteObjectType +from syncify.shared.remote.library import RemoteLibrary from syncify.spotify.api import SpotifyAPI from syncify.spotify.config import SPOTIFY_OBJECT_CLASSES from syncify.spotify.object import SpotifyTrack, SpotifyCollection, SpotifyPlaylist, SpotifyAlbum, SpotifyArtist diff --git a/src/syncify/spotify/object.py b/src/syncify/spotify/object.py index d59e8903..50da0692 100644 --- a/src/syncify/spotify/object.py +++ b/src/syncify/spotify/object.py @@ -6,15 +6,15 @@ from datetime import datetime from typing import Any, Self -from syncify.remote.enums import RemoteObjectType, RemoteIDType -from syncify.remote.object import RemoteCollection, RemoteCollectionLoader, RemoteTrack -from syncify.remote.object import RemotePlaylist, RemoteAlbum, RemoteArtist +from syncify.shared.remote.enums import RemoteObjectType, RemoteIDType +from syncify.shared.remote.object import RemoteCollection, RemoteCollectionLoader, RemoteTrack +from syncify.shared.remote.object import RemotePlaylist, RemoteAlbum, RemoteArtist from syncify.spotify.api import SpotifyAPI from syncify.spotify.exception import SpotifyCollectionError from syncify.spotify.base import SpotifyObject, SpotifyItem from syncify.spotify.processors.wrangle import SpotifyDataWrangler -from syncify.utils import UnitCollection -from syncify.utils.helpers import to_collection +from syncify.shared.types import UnitCollection +from syncify.shared.utils import to_collection class SpotifyItemWranglerMixin(SpotifyItem, SpotifyDataWrangler, metaclass=ABCMeta): diff --git a/src/syncify/spotify/processors/processors.py b/src/syncify/spotify/processors/processors.py index 5467543f..0a5bfca0 100644 --- a/src/syncify/spotify/processors/processors.py +++ b/src/syncify/spotify/processors/processors.py @@ -1,6 +1,6 @@ -from syncify.remote.config import RemoteObjectClasses -from syncify.remote.processors.check import RemoteItemChecker -from syncify.remote.processors.search import RemoteItemSearcher +from syncify.shared.remote.config import RemoteObjectClasses +from syncify.shared.remote.processors.check import RemoteItemChecker +from syncify.shared.remote.processors.search import RemoteItemSearcher from syncify.spotify.config import SPOTIFY_OBJECT_CLASSES from syncify.spotify.processors.wrangle import SpotifyDataWrangler diff --git a/src/syncify/spotify/processors/wrangle.py b/src/syncify/spotify/processors/wrangle.py index 0adf530c..cc84ad05 100644 --- a/src/syncify/spotify/processors/wrangle.py +++ b/src/syncify/spotify/processors/wrangle.py @@ -2,11 +2,11 @@ from typing import Any from urllib.parse import urlparse -from syncify.exception import SyncifyEnumError -from syncify.remote.enums import RemoteIDType, RemoteObjectType -from syncify.remote.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.remote.types import APIMethodInputType +from syncify.shared.exception import SyncifyEnumError +from syncify.shared.remote.enums import RemoteIDType, RemoteObjectType +from syncify.shared.remote.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.remote.api import APIMethodInputType from syncify.spotify import URL_API, URL_EXT, SPOTIFY_UNAVAILABLE_URI, SpotifyRemote diff --git a/src/syncify/utils/__init__.py b/src/syncify/utils/__init__.py deleted file mode 100644 index 3693b3ae..00000000 --- a/src/syncify/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .helpers import Number, UnitIterable, UnitCollection, UnitSequence, UnitMutableSequence diff --git a/tests/__resources/test_logging.yml b/tests/__resources/test_logging.yml index b97bba87..f4b1c38d 100644 --- a/tests/__resources/test_logging.yml +++ b/tests/__resources/test_logging.yml @@ -16,10 +16,10 @@ formatters: filters: console: - (): "syncify.utils.logger.LogConsoleFilter" + (): "syncify.shared.logger.LogConsoleFilter" module_width: 40 file: - (): "syncify.utils.logger.LogFileFilter" + (): "syncify.shared.logger.LogFileFilter" module_width: 40 handlers: @@ -43,7 +43,7 @@ handlers: level: INFO_EXTRA file: - class: syncify.utils.logger.CurrentTimeRotatingFileHandler + class: syncify.shared.logger.CurrentTimeRotatingFileHandler level: DEBUG formatter: extended filters: ["file"] diff --git a/tests/conftest.py b/tests/conftest.py index 2ed448c5..93cf3cfc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,13 +2,14 @@ import os import shutil from os.path import join, basename, dirname -from typing import Any import pytest import yaml from _pytest.fixtures import SubRequest -from syncify.api import RequestHandler +from syncify import MODULE_ROOT +from syncify.shared.api.request import RequestHandler +from syncify.shared.logger import SyncifyLogger from syncify.spotify.api import SpotifyAPI from syncify.spotify.processors.wrangle import SpotifyDataWrangler from tests.spotify.api.mock import SpotifyMock @@ -21,22 +22,15 @@ def pytest_configure(config: pytest.Config): config_file = join(dirname(dirname(__file__)), "logging.yml") with open(config_file, "r") as file: log_config = yaml.full_load(file) + log_config.pop("compact", False) + SyncifyLogger.disable_bars = True + SyncifyLogger.compact = True for formatter in log_config["formatters"].values(): # ensure ANSI colour codes in format are recognised formatter["format"] = formatter["format"].replace(r"\33", "\33") - def remove_file_handler(c: dict[str, Any]) -> None: - """Remove all config for file handlers""" - for k, v in c.items(): - if k == "handlers" and isinstance(v, list) and "file" in v: - v.pop(v.index("file")) - elif k == "handlers" and isinstance(v, dict) and "file" in v: - v.pop("file") - elif isinstance(v, dict): - remove_file_handler(v) - - remove_file_handler(log_config) + log_config["loggers"][MODULE_ROOT] = log_config["loggers"]["test"] logging.config.dictConfig(log_config) diff --git a/tests/local/conftest.py b/tests/local/conftest.py index 44a7e6a9..f24d5139 100644 --- a/tests/local/conftest.py +++ b/tests/local/conftest.py @@ -2,7 +2,7 @@ from pytest_lazyfixture import lazy_fixture from syncify.local.track import LocalTrack, FLAC, MP3, M4A, WMA -from syncify.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler from syncify.spotify.processors.wrangle import SpotifyDataWrangler from tests.local.utils import path_track_flac, path_track_mp3, path_track_m4a, path_track_wma, path_track_all diff --git a/tests/local/library/test_local_library.py b/tests/local/library/test_local_library.py index 1491bbff..db168e1f 100644 --- a/tests/local/library/test_local_library.py +++ b/tests/local/library/test_local_library.py @@ -6,11 +6,12 @@ from syncify.local.library import LocalLibrary from syncify.local.track import LocalTrack -from tests.abstract.misc import BasicFilter -from tests.local.library.utils import LocalLibraryTester +from tests.shared.core.misc import BasicFilter +from tests.local.library.testers import LocalLibraryTester from tests.local.playlist.utils import path_playlist_resources, path_playlist_m3u from tests.local.playlist.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra -from tests.local.utils import path_track_resources, path_track_all, random_tracks, random_track +from tests.local.utils import path_track_resources, path_track_all +from tests.local.track.utils import random_track, random_tracks class TestLocalLibrary(LocalLibraryTester): diff --git a/tests/local/library/test_musicbee.py b/tests/local/library/test_musicbee.py index f4e0c874..eebbf284 100644 --- a/tests/local/library/test_musicbee.py +++ b/tests/local/library/test_musicbee.py @@ -6,14 +6,15 @@ from syncify.local.exception import MusicBeeError, FileDoesNotExistError from syncify.local.library import LocalLibrary, MusicBee # noinspection PyProtectedMember -from syncify.local.library._musicbee import XMLLibraryParser +from syncify.local.library.musicbee import XMLLibraryParser from syncify.local.track import LocalTrack -from syncify.remote.processors.wrangle import RemoteDataWrangler -from tests.local.library.utils import LocalLibraryTester +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from tests.local.library.testers import LocalLibraryTester from tests.local.library.utils import path_library_resources from tests.local.playlist.utils import path_playlist_resources, path_playlist_m3u from tests.local.playlist.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra -from tests.local.utils import random_track, path_track_all, path_track_mp3, path_track_flac, path_track_wma +from tests.local.utils import path_track_all, path_track_mp3, path_track_flac, path_track_wma +from tests.local.track.utils import random_track from tests.utils import path_resources library_filename = "musicbee_library.xml" @@ -184,8 +185,7 @@ def test_save(self, path: str, remote_wrangler: RemoteDataWrangler): # these keys fail on other systems, ignore them in line checks ignore_keys = ["Music Folder", "Date Modified", "Location"] with open(library_filepath, "r") as f_in, open(path, "r") as f_out: - for i, (line_in, line_out) in enumerate(zip(f_in, f_out)): + for line_in, line_out in zip(f_in, f_out): if any(f"{key}" in line_in for key in ignore_keys): continue - # print(i, line_out.rstrip()) assert line_in == line_out diff --git a/tests/local/library/testers.py b/tests/local/library/testers.py new file mode 100644 index 00000000..90199270 --- /dev/null +++ b/tests/local/library/testers.py @@ -0,0 +1,42 @@ +from abc import ABCMeta, abstractmethod +from os.path import splitext, basename + +from syncify.local.library import LocalLibrary +from tests.local.playlist.utils import path_playlist_resources, path_playlist_m3u +from tests.local.playlist.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra +from tests.local.track.testers import LocalCollectionTester +from tests.local.utils import path_track_resources, path_track_all +from tests.shared.core.collection import LibraryTester + + +class LocalLibraryTester(LibraryTester, LocalCollectionTester, metaclass=ABCMeta): + + dict_json_equal = False + + @abstractmethod + def blank_library(self) -> LocalLibrary: + """A blank :py:class:`LocalLibrary` implementation to be tested.""" + raise NotImplementedError + + @staticmethod + def test_blank_library(blank_library: LocalLibrary) -> None: + """General tests to run for every implementation of :py:class:`LocalLibrary`""" + assert blank_library.library_folder is None + assert len(blank_library._track_paths) == 0 + blank_library.library_folder = path_track_resources + assert blank_library.library_folder == path_track_resources + assert blank_library._track_paths == path_track_all + + assert blank_library.playlist_folder is None + assert blank_library._playlist_paths is None + blank_library.playlist_folder = path_playlist_resources + assert blank_library.playlist_folder == path_playlist_resources + assert blank_library._playlist_paths == { + splitext(basename(path_playlist_m3u).casefold())[0]: path_playlist_m3u, + splitext(basename(path_playlist_xautopf_bp).casefold())[0]: path_playlist_xautopf_bp, + splitext(basename(path_playlist_xautopf_ra).casefold())[0]: path_playlist_xautopf_ra, + } + + def test_merge_playlists(self, library: LocalLibrary): + # TODO: write merge_playlists tests + pass diff --git a/tests/local/library/utils.py b/tests/local/library/utils.py index 580d08cd..0a479ff1 100644 --- a/tests/local/library/utils.py +++ b/tests/local/library/utils.py @@ -1,45 +1,5 @@ -from abc import ABCMeta, abstractmethod -from os.path import splitext, basename, join, dirname +from os.path import basename, join, dirname -from syncify.local.library import LocalLibrary -from tests.abstract.collection import LibraryTester -from tests.local.playlist.utils import path_playlist_resources, path_playlist_m3u -from tests.local.playlist.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra -from tests.local.test_local_collection import LocalCollectionTester -from tests.local.utils import path_track_resources, path_track_all from tests.utils import path_resources path_library_resources = join(path_resources, basename(dirname(__file__))) - - -class LocalLibraryTester(LibraryTester, LocalCollectionTester, metaclass=ABCMeta): - - dict_json_equal = False - - @abstractmethod - def blank_library(self) -> LocalLibrary: - """A blank :py:class:`LocalLibrary` implementation to be tested.""" - raise NotImplementedError - - @staticmethod - def test_blank_library(blank_library: LocalLibrary) -> None: - """General tests to run for every implementation of :py:class:`LocalLibrary`""" - assert blank_library.library_folder is None - assert len(blank_library._track_paths) == 0 - blank_library.library_folder = path_track_resources - assert blank_library.library_folder == path_track_resources - assert blank_library._track_paths == path_track_all - - assert blank_library.playlist_folder is None - assert blank_library._playlist_paths is None - blank_library.playlist_folder = path_playlist_resources - assert blank_library.playlist_folder == path_playlist_resources - assert blank_library._playlist_paths == { - splitext(basename(path_playlist_m3u).casefold())[0]: path_playlist_m3u, - splitext(basename(path_playlist_xautopf_bp).casefold())[0]: path_playlist_xautopf_bp, - splitext(basename(path_playlist_xautopf_ra).casefold())[0]: path_playlist_xautopf_ra, - } - - def test_merge_playlists(self, library: LocalLibrary): - # TODO: write merge_playlists tests - pass diff --git a/tests/local/playlist/test_local_playlist_match.py b/tests/local/playlist/test_local_playlist_match.py index 6b67aad0..15ca01ab 100644 --- a/tests/local/playlist/test_local_playlist_match.py +++ b/tests/local/playlist/test_local_playlist_match.py @@ -4,13 +4,14 @@ import pytest import xmltodict -from syncify.fields import LocalTrackField +from syncify.local.track.fields import LocalTrackField from syncify.local.playlist import LocalMatcher from syncify.local.track import LocalTrack from syncify.processors.compare import Comparer -from tests.abstract.misc import PrettyPrinterTester +from tests.shared.core.misc import PrettyPrinterTester from tests.local.playlist.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra, path_playlist_resources -from tests.local.utils import random_tracks, path_track_wma, path_track_flac, path_track_mp3, path_track_resources +from tests.local.utils import path_track_wma, path_track_flac, path_track_mp3, path_track_resources +from tests.local.track.utils import random_tracks from tests.utils import random_str, path_resources diff --git a/tests/local/playlist/test_m3u.py b/tests/local/playlist/test_m3u.py index 19d2d82b..6a111127 100644 --- a/tests/local/playlist/test_m3u.py +++ b/tests/local/playlist/test_m3u.py @@ -5,11 +5,13 @@ import pytest +from tests.local.track.utils import random_track, random_tracks +from tests.local.playlist.testers import LocalPlaylistTester from syncify.local.exception import InvalidFileType from syncify.local.playlist import M3U from syncify.local.track import LocalTrack -from tests.local.playlist.utils import LocalPlaylistTester, path_playlist_m3u, path_resources -from tests.local.utils import random_track, random_tracks, path_track_all +from tests.local.playlist.utils import path_playlist_m3u, path_resources +from tests.local.utils import path_track_all from tests.utils import path_txt diff --git a/tests/local/playlist/test_xautopf.py b/tests/local/playlist/test_xautopf.py index bcde2cf3..8c0f5278 100644 --- a/tests/local/playlist/test_xautopf.py +++ b/tests/local/playlist/test_xautopf.py @@ -5,12 +5,14 @@ import pytest -from syncify.fields import LocalTrackField +from tests.local.track.utils import random_track, random_tracks +from tests.local.playlist.testers import LocalPlaylistTester +from syncify.local.track.fields import LocalTrackField from syncify.local.exception import InvalidFileType from syncify.local.playlist import XAutoPF from syncify.local.track import LocalTrack -from tests.local.playlist.utils import LocalPlaylistTester, path_playlist_xautopf_ra, path_playlist_xautopf_bp -from tests.local.utils import random_tracks, path_track_flac, path_track_wma, random_track +from tests.local.playlist.utils import path_playlist_xautopf_ra, path_playlist_xautopf_bp +from tests.local.utils import path_track_flac, path_track_wma from tests.utils import path_txt, path_resources diff --git a/tests/local/playlist/testers.py b/tests/local/playlist/testers.py new file mode 100644 index 00000000..0e285ff3 --- /dev/null +++ b/tests/local/playlist/testers.py @@ -0,0 +1,8 @@ +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/local/playlist/utils.py b/tests/local/playlist/utils.py index 3fa06462..2d41ed45 100644 --- a/tests/local/playlist/utils.py +++ b/tests/local/playlist/utils.py @@ -1,15 +1,8 @@ -from abc import ABCMeta from os.path import join, basename, dirname -from tests.abstract.collection import PlaylistTester -from tests.local.test_local_collection import LocalCollectionTester from tests.utils import path_resources path_playlist_resources = join(path_resources, str(basename(dirname(__file__)))) path_playlist_m3u = join(path_playlist_resources, "Simple Playlist.m3u") path_playlist_xautopf_bp = join(path_playlist_resources, "The Best Playlist Ever.xautopf") path_playlist_xautopf_ra = join(path_playlist_resources, "Recently Added.xautopf") - - -class LocalPlaylistTester(PlaylistTester, LocalCollectionTester, metaclass=ABCMeta): - pass diff --git a/tests/local/test_local_collection.py b/tests/local/test_local_collection.py index cb4e4f9d..10667b94 100644 --- a/tests/local/test_local_collection.py +++ b/tests/local/test_local_collection.py @@ -1,70 +1,16 @@ -from abc import ABCMeta -from collections.abc import Iterable, Collection from copy import copy -from random import randrange, sample import pytest -from syncify.exception import SyncifyKeyError -from syncify.local.collection import LocalFolder, LocalAlbum, LocalArtist, LocalGenres, LocalCollection +from tests.local.track.utils import random_track, random_tracks +from tests.local.track.testers import LocalCollectionTester +from syncify.local.collection import LocalFolder, LocalAlbum, LocalArtist, LocalGenres from syncify.local.exception import LocalCollectionError from syncify.local.track import LocalTrack -from syncify.spotify.object import SpotifyTrack -from tests.abstract.collection import ItemCollectionTester -from tests.local.utils import random_tracks, random_track, path_track_resources, path_track_all -from tests.spotify.api.mock import SpotifyMock +from tests.local.utils import path_track_resources, path_track_all from tests.utils import random_str -class LocalCollectionTester(ItemCollectionTester, metaclass=ABCMeta): - - @pytest.fixture - def collection_merge_items(self) -> Iterable[LocalTrack]: - return random_tracks(randrange(5, 10)) - - @pytest.fixture(scope="module") - def collection_merge_invalid(self, spotify_mock: SpotifyMock) -> Collection[SpotifyTrack]: - return tuple(SpotifyTrack(response) for response in sample(spotify_mock.tracks, k=5)) - - def test_collection_getitem_dunder_method( - self, collection: LocalCollection, collection_merge_items: Iterable[LocalTrack] - ): - """:py:class:`ItemCollection` __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] - assert collection[:2] == collection.items[:2] - assert collection[idx] == collection.items[idx] == item - - assert collection[item] == item - assert collection[item.name] == item - assert collection[item.path] == item - - if collection.remote_wrangler is not None: - assert collection[item.uri] == item - else: - with pytest.raises(SyncifyKeyError): - assert collection[item.uri] - - invalid_track = next(item for item in collection_merge_items) - with pytest.raises(SyncifyKeyError): - assert collection[invalid_track] - with pytest.raises(SyncifyKeyError): - assert collection[invalid_track.name] - with pytest.raises(SyncifyKeyError): - assert collection[invalid_track.path] - with pytest.raises(SyncifyKeyError): - assert collection[invalid_track.uri] - - @staticmethod - def test_merge_tracks(collection: LocalCollection, collection_merge_items: Collection[SpotifyTrack]): - length = len(collection.items) - assert all(item not in collection.items for item in collection_merge_items) - - collection.merge_tracks(collection_merge_items) - assert len(collection.items) == length - - class TestLocalFolder(LocalCollectionTester): name = "folder name" diff --git a/tests/local/test_track.py b/tests/local/track/test_track.py similarity index 95% rename from tests/local/test_track.py rename to tests/local/track/test_track.py index 8389f9a8..1071a1fa 100644 --- a/tests/local/test_track.py +++ b/tests/local/track/test_track.py @@ -4,15 +4,15 @@ import pytest -from syncify.abstract import Item -from syncify.abstract.object import Track -from syncify.exception import SyncifyKeyError -from syncify.fields import LocalTrackField -from syncify.local import open_image +from syncify.shared.core.base import Item +from syncify.shared.core.object import Track +from syncify.shared.exception import SyncifyKeyError +from syncify.local.track.fields import LocalTrackField +from syncify.local.file import open_image from syncify.local.exception import InvalidFileType, FileDoesNotExistError from syncify.local.track import LocalTrack, load_track, FLAC, M4A, MP3, WMA -from syncify.remote.enums import RemoteObjectType -from tests.abstract.base import ItemTester +from syncify.shared.remote.enums import RemoteObjectType +from tests.shared.core.base import ItemTester from tests.local.utils import path_track_all, path_track_img, path_track_resources from tests.spotify.utils import random_uri from tests.utils import path_txt diff --git a/tests/local/track/testers.py b/tests/local/track/testers.py new file mode 100644 index 00000000..33419eee --- /dev/null +++ b/tests/local/track/testers.py @@ -0,0 +1,63 @@ +from abc import ABCMeta +from collections.abc import Iterable, Collection +from random import randrange, sample + +import pytest + +from tests.local.track.utils import random_tracks +from syncify.local.collection import LocalCollection +from syncify.local.track import LocalTrack +# noinspection PyProtectedMember +from syncify.shared.exception import SyncifyKeyError +from syncify.spotify.object import SpotifyTrack +from tests.shared.core.collection import ItemCollectionTester +from tests.spotify.api.mock import SpotifyMock + + +class LocalCollectionTester(ItemCollectionTester, metaclass=ABCMeta): + + @pytest.fixture + def collection_merge_items(self) -> Iterable[LocalTrack]: + return random_tracks(randrange(5, 10)) + + @pytest.fixture(scope="module") + def collection_merge_invalid(self, spotify_mock: SpotifyMock) -> Collection[SpotifyTrack]: + return tuple(SpotifyTrack(response) for response in sample(spotify_mock.tracks, k=5)) + + def test_collection_getitem_dunder_method( + self, collection: LocalCollection, collection_merge_items: Iterable[LocalTrack] + ): + """:py:class:`ItemCollection` __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] + assert collection[:2] == collection.items[:2] + assert collection[idx] == collection.items[idx] == item + + assert collection[item] == item + assert collection[item.name] == item + assert collection[item.path] == item + + if collection.remote_wrangler is not None: + assert collection[item.uri] == item + else: + with pytest.raises(SyncifyKeyError): + assert collection[item.uri] + + invalid_track = next(item for item in collection_merge_items) + with pytest.raises(SyncifyKeyError): + assert collection[invalid_track] + with pytest.raises(SyncifyKeyError): + assert collection[invalid_track.name] + with pytest.raises(SyncifyKeyError): + assert collection[invalid_track.path] + with pytest.raises(SyncifyKeyError): + assert collection[invalid_track.uri] + + @staticmethod + def test_merge_tracks(collection: LocalCollection, collection_merge_items: Collection[SpotifyTrack]): + length = len(collection.items) + assert all(item not in collection.items for item in collection_merge_items) + + collection.merge_tracks(collection_merge_items) + assert len(collection.items) == length diff --git a/tests/local/track/utils.py b/tests/local/track/utils.py new file mode 100644 index 00000000..951e920d --- /dev/null +++ b/tests/local/track/utils.py @@ -0,0 +1,75 @@ +import string +from datetime import datetime +from os.path import join +from random import choice, randrange, randint + +import mutagen +from dateutil.relativedelta import relativedelta + +from syncify.local.track import TRACK_CLASSES, LocalTrack +# noinspection PyProtectedMember +from syncify.local.track._base.writer import TagWriter +from tests.spotify.utils import random_uri +from tests.utils import random_str, random_dt, random_genres +from tests.local.utils import remote_wrangler, path_track_resources + + +class MutagenMock(mutagen.FileType): + class MutagenInfoMock(mutagen.StreamInfo): + def __init__(self): + self.length = 0 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mock = True + self.info = self.MutagenInfoMock() + self.pictures = [] + + +def random_track[T: LocalTrack](cls: type[T] | None = None) -> T: + """Generates a new, random track of the given class.""" + if cls is None: + cls = choice(tuple(TRACK_CLASSES)) + track = cls.__new__(cls) + TagWriter.__init__(track, remote_wrangler=remote_wrangler) + track._available_paths = set() + track._available_paths_lower = set() + + track._file = MutagenMock() + track.file.info.length = randint(30, 600) + + track.title = random_str(30, 50) + track.artist = random_str(30, 50) + track.album = random_str(30, 50) + track.album_artist = random_str(30, 50) + track.track_number = randrange(1, 20) + track.track_total = randint(track.track_number, 20) + track.genres = random_genres() + track.date = random_dt() + track.bpm = randint(6000, 15000) / 100 + track.key = choice(string.ascii_uppercase[:7]) + track.disc_number = randrange(1, 8) + track.disc_total = randint(track.disc_number, 20) + track.compilation = choice([True, False]) + track.comments = [random_str(20, 50) for _ in range(randrange(3))] + + has_uri = choice([True, False]) + track.uri = random_uri() if has_uri else remote_wrangler.unavailable_uri_dummy + + track._image_links = {} + track.has_image = False + + filename_ext = f"{str(track.track_number).zfill(2)} - {track.title}" + choice(tuple(track.valid_extensions)) + track.file.filename = join(path_track_resources, random_str(30, 50), filename_ext) + + track.date_added = datetime.now() - relativedelta(days=randrange(8, 20), hours=randrange(1, 24)) + track.last_played = datetime.now() - relativedelta(days=randrange(1, 6), hours=randrange(1, 24)) + track.play_count = randrange(200) + track.rating = randrange(0, 100) + + return track + + +def random_tracks[T: LocalTrack](number: int | None = None, cls: type[T] | None = None) -> list[T]: + """Generates a ``number`` of random tracks of the given class.""" + return [random_track(cls=cls) for _ in range(number or randrange(2, 20))] diff --git a/tests/local/utils.py b/tests/local/utils.py index a4d74c15..67cb2ccb 100644 --- a/tests/local/utils.py +++ b/tests/local/utils.py @@ -1,17 +1,8 @@ -import string -from datetime import datetime from os.path import join -from random import choice, randrange, randint -import mutagen -from dateutil.relativedelta import relativedelta - -from syncify.local.track import TRACK_CLASSES, LocalTrack -# noinspection PyProtectedMember -from syncify.local.track._base.writer import TagWriter +from syncify.local.track import TRACK_CLASSES from syncify.spotify.processors.wrangle import SpotifyDataWrangler -from tests.spotify.utils import random_uri -from tests.utils import path_resources, random_str, random_dt +from tests.utils import path_resources path_track_resources = join(path_resources, "track") path_track_flac = join(path_track_resources, "noise_flac.flac") @@ -22,64 +13,3 @@ path_track_all: set[str] = {path for c in TRACK_CLASSES for path in c.get_filepaths(path_track_resources)} remote_wrangler = SpotifyDataWrangler() - - -class MutagenMock(mutagen.FileType): - class MutagenInfoMock(mutagen.StreamInfo): - def __init__(self): - self.length = 0 - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.mock = True - self.info = self.MutagenInfoMock() - self.pictures = [] - - -def random_track[T: LocalTrack](cls: type[T] | None = None) -> T: - """Generates a new, random track of the given class.""" - if cls is None: - cls = choice(tuple(TRACK_CLASSES)) - track = cls.__new__(cls) - TagWriter.__init__(track, remote_wrangler=remote_wrangler) - track._available_paths = set() - track._available_paths_lower = set() - - track._file = MutagenMock() - track.file.info.length = randint(30, 600) - - track.title = random_str(30, 50) - track.artist = random_str(30, 50) - track.album = random_str(30, 50) - track.album_artist = random_str(30, 50) - track.track_number = randrange(1, 20) - track.track_total = randint(track.track_number, 20) - track.genres = [random_str(20, 50) for _ in range(randrange(7))] - track.date = random_dt() - track.bpm = randint(6000, 15000) / 100 - track.key = choice(string.ascii_uppercase[:7]) - track.disc_number = randrange(1, 8) - track.disc_total = randint(track.disc_number, 20) - track.compilation = choice([True, False]) - track.comments = [random_str(20, 50) for _ in range(randrange(3))] - - has_uri = choice([True, False]) - track.uri = random_uri() if has_uri else remote_wrangler.unavailable_uri_dummy - - track._image_links = {} - track.has_image = False - - filename_ext = f"{str(track.track_number).zfill(2)} - {track.title}" + choice(tuple(track.valid_extensions)) - track.file.filename = join(path_track_resources, random_str(30, 50), filename_ext) - - track.date_added = datetime.now() - relativedelta(days=randrange(8, 20), hours=randrange(1, 24)) - track.last_played = datetime.now() - relativedelta(days=randrange(1, 6), hours=randrange(1, 24)) - track.play_count = randrange(200) - track.rating = randrange(0, 100) - - return track - - -def random_tracks[T: LocalTrack](number: int | None = None, cls: type[T] | None = None) -> list[T]: - """Generates a ``number`` of random tracks of the given class.""" - return [random_track(cls=cls) for _ in range(number or randrange(2, 20))] diff --git a/tests/processors/test_compare.py b/tests/processors/test_compare.py index db230f47..769c45bf 100644 --- a/tests/processors/test_compare.py +++ b/tests/processors/test_compare.py @@ -3,13 +3,14 @@ import pytest import xmltodict -from syncify.fields import TrackField, LocalTrackField +from syncify.shared.fields import TrackField +from syncify.local.track.fields import LocalTrackField from syncify.local.track import MP3, M4A, FLAC from syncify.processors.compare import Comparer from syncify.processors.exception import ItemComparerError, ProcessorLookupError -from tests.abstract.misc import PrettyPrinterTester +from tests.shared.core.misc import PrettyPrinterTester from tests.local.playlist.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra -from tests.local.utils import random_track +from tests.local.track.utils import random_track class TestItemComparer(PrettyPrinterTester): diff --git a/tests/processors/test_limit.py b/tests/processors/test_limit.py index 095fd27b..6f1ab4eb 100644 --- a/tests/processors/test_limit.py +++ b/tests/processors/test_limit.py @@ -5,9 +5,9 @@ from syncify.local.track import LocalTrack from syncify.processors.limit import ItemLimiter, LimitType -from tests.abstract.misc import PrettyPrinterTester +from tests.shared.core.misc import PrettyPrinterTester from tests.local.playlist.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra -from tests.local.utils import random_tracks +from tests.local.track.utils import random_tracks from tests.utils import random_file diff --git a/tests/processors/test_match.py b/tests/processors/test_match.py index f8e14f04..2826f837 100644 --- a/tests/processors/test_match.py +++ b/tests/processors/test_match.py @@ -1,10 +1,10 @@ import pytest -from syncify.abstract.enums import TagFields as Tag +from syncify.shared.core.enums import TagFields as Tag from syncify.local.track import LocalTrack from syncify.processors.match import ItemMatcher, CleanTagConfig -from tests.abstract.misc import PrettyPrinterTester -from tests.local.utils import random_track +from tests.shared.core.misc import PrettyPrinterTester +from tests.local.track.utils import random_track class TestItemMatcher(PrettyPrinterTester): diff --git a/tests/processors/test_sort.py b/tests/processors/test_sort.py index 9249c74a..fb6425d0 100644 --- a/tests/processors/test_sort.py +++ b/tests/processors/test_sort.py @@ -5,13 +5,14 @@ import pytest import xmltodict -from syncify.fields import TrackField, LocalTrackField +from syncify.shared.fields import TrackField +from syncify.local.track.fields import LocalTrackField from syncify.local.track import LocalTrack from syncify.processors.sort import ItemSorter, ShuffleMode, ShuffleBy -from syncify.utils.helpers import strip_ignore_words -from tests.abstract.misc import PrettyPrinterTester +from syncify.shared.utils import strip_ignore_words +from tests.shared.core.misc import PrettyPrinterTester from tests.local.playlist.utils import path_playlist_xautopf_bp, path_playlist_xautopf_ra -from tests.local.utils import random_tracks +from tests.local.track.utils import random_tracks class TestItemSorter(PrettyPrinterTester): diff --git a/tests/processors/test_time.py b/tests/processors/test_time.py index 52c5ba0b..28d89f58 100644 --- a/tests/processors/test_time.py +++ b/tests/processors/test_time.py @@ -4,7 +4,7 @@ from dateutil.relativedelta import relativedelta from syncify.processors.time import TimeMapper -from tests.abstract.misc import PrettyPrinterTester +from tests.shared.core.misc import PrettyPrinterTester class TestTimeMapper(PrettyPrinterTester): diff --git a/tests/api/test_authorise.py b/tests/shared/api/test_authorise.py similarity index 96% rename from tests/api/test_authorise.py rename to tests/shared/api/test_authorise.py index 20a7f042..5b7b46b7 100644 --- a/tests/api/test_authorise.py +++ b/tests/shared/api/test_authorise.py @@ -10,9 +10,9 @@ from pytest_mock import MockerFixture from requests_mock import Mocker -from syncify.api import APIAuthoriser -from syncify.api.exception import APIError -from tests.api.utils import path_token +from syncify.shared.api.authorise import APIAuthoriser +from syncify.shared.api.exception import APIError +from tests.shared.api.utils import path_token class TestAPIAuthoriser: @@ -109,7 +109,7 @@ def check_url(url: str): socket_listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) requests_mock.post(user_url) - mocker.patch("syncify.api._authorise.webopen", new=check_url) + mocker.patch("syncify.shared.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/api/test_request.py b/tests/shared/api/test_request.py similarity index 96% rename from tests/api/test_request.py rename to tests/shared/api/test_request.py index 85c24c43..fff30541 100644 --- a/tests/api/test_request.py +++ b/tests/shared/api/test_request.py @@ -12,8 +12,8 @@ # noinspection PyProtectedMember,PyUnresolvedReferences from requests_mock.response import _Context as Context -from syncify.api import RequestHandler -from syncify.api.exception import APIError +from syncify.shared.api.request import RequestHandler +from syncify.shared.api.exception import APIError class TestRequestHandler: diff --git a/tests/api/utils.py b/tests/shared/api/utils.py similarity index 100% rename from tests/api/utils.py rename to tests/shared/api/utils.py diff --git a/tests/abstract/base.py b/tests/shared/core/base.py similarity index 87% rename from tests/abstract/base.py rename to tests/shared/core/base.py index bd45236a..167b6798 100644 --- a/tests/abstract/base.py +++ b/tests/shared/core/base.py @@ -2,9 +2,9 @@ import pytest -from syncify.abstract import Item -from syncify.abstract.misc import PrettyPrinter -from tests.abstract.misc import PrettyPrinterTester +from syncify.shared.core.base import Item +from syncify.shared.core.misc import PrettyPrinter +from tests.shared.core.misc import PrettyPrinterTester class ItemTester(PrettyPrinterTester, metaclass=ABCMeta): diff --git a/tests/abstract/collection.py b/tests/shared/core/collection.py similarity index 93% rename from tests/abstract/collection.py rename to tests/shared/core/collection.py index fab0ada8..3fa4e468 100644 --- a/tests/abstract/collection.py +++ b/tests/shared/core/collection.py @@ -4,14 +4,14 @@ import pytest -from syncify.abstract import Item -from syncify.abstract.collection import ItemCollection -from syncify.abstract.misc import PrettyPrinter -from syncify.abstract.object import BasicCollection, Library, Playlist -from syncify.exception import SyncifyTypeError -from syncify.remote.library import RemoteLibrary -from syncify.remote.object import RemoteCollectionLoader -from tests.abstract.misc import PrettyPrinterTester, BasicFilter +from syncify.shared.core.base import Item +from syncify.shared.core.collection import ItemCollection +from syncify.shared.core.misc import PrettyPrinter +from syncify.shared.core.object import BasicCollection, Library, Playlist +from syncify.shared.exception import SyncifyTypeError +from syncify.shared.remote.library import RemoteLibrary +from syncify.shared.remote.object import RemoteCollectionLoader +from tests.shared.core.misc import PrettyPrinterTester, BasicFilter class ItemCollectionTester(PrettyPrinterTester, metaclass=ABCMeta): diff --git a/tests/abstract/enums.py b/tests/shared/core/enums.py similarity index 93% rename from tests/abstract/enums.py rename to tests/shared/core/enums.py index 5867e024..37c7a768 100644 --- a/tests/abstract/enums.py +++ b/tests/shared/core/enums.py @@ -1,8 +1,8 @@ from abc import ABC, abstractmethod, ABCMeta from collections.abc import Container -from syncify.abstract import NamedObject -from syncify.abstract.enums import SyncifyEnum, Fields, TagField, ALL_FIELDS, Field +from syncify.shared.core.base import NamedObject +from syncify.shared.core.enums import SyncifyEnum, Fields, TagField, ALL_FIELDS, Field class EnumTester(ABC): diff --git a/tests/abstract/misc.py b/tests/shared/core/misc.py similarity index 89% rename from tests/abstract/misc.py rename to tests/shared/core/misc.py index 92905e11..2d2818e3 100644 --- a/tests/abstract/misc.py +++ b/tests/shared/core/misc.py @@ -1,9 +1,10 @@ import json import re from abc import ABC, abstractmethod -from typing import Iterable, Collection, Any +from collections.abc import Iterable, Collection +from typing import Any -from syncify.abstract.misc import PrettyPrinter, Filter +from syncify.shared.core.misc import PrettyPrinter, Filter class BasicFilter(Filter[Any]): diff --git a/tests/remote/library/library.py b/tests/shared/remote/library.py similarity index 94% rename from tests/remote/library/library.py rename to tests/shared/remote/library.py index 160927cb..70bdb93e 100644 --- a/tests/remote/library/library.py +++ b/tests/shared/remote/library.py @@ -3,14 +3,14 @@ from copy import copy, deepcopy from typing import Any -from syncify.abstract import Item -from syncify.abstract.object import Playlist -from syncify.remote.library import RemoteLibrary -from syncify.remote.object import RemoteTrack -from tests.abstract.collection import LibraryTester -from tests.local.utils import random_tracks -from tests.remote.library.object import RemoteCollectionTester -from tests.remote.utils import RemoteMock +from syncify.shared.core.base import Item +from syncify.shared.core.object import Playlist +from syncify.shared.remote.library import RemoteLibrary +from syncify.shared.remote.object import RemoteTrack +from tests.shared.core.collection import LibraryTester +from tests.local.track.utils import random_tracks +from tests.shared.remote.object import RemoteCollectionTester +from tests.shared.remote.utils import RemoteMock class RemoteLibraryTester(RemoteCollectionTester, LibraryTester, metaclass=ABCMeta): diff --git a/tests/remote/library/object.py b/tests/shared/remote/object.py similarity index 94% rename from tests/remote/library/object.py rename to tests/shared/remote/object.py index 3c8a9d64..a9a489e6 100644 --- a/tests/remote/library/object.py +++ b/tests/shared/remote/object.py @@ -4,14 +4,14 @@ import pytest -from syncify.exception import SyncifyKeyError +from syncify.shared.exception import SyncifyKeyError from syncify.local.track import LocalTrack -from syncify.remote.api import RemoteAPI -from syncify.remote.base import RemoteItem -from syncify.remote.object import RemoteTrack, RemoteCollection, RemotePlaylist -from tests.abstract.collection import ItemCollectionTester, PlaylistTester -from tests.local.utils import random_tracks -from tests.remote.utils import RemoteMock +from syncify.shared.remote.api import RemoteAPI +from syncify.shared.remote.base import RemoteItem +from syncify.shared.remote.object import RemoteTrack, RemoteCollection, RemotePlaylist +from tests.shared.core.collection import ItemCollectionTester, PlaylistTester +from tests.local.track.utils import random_tracks +from tests.shared.remote.utils import RemoteMock class RemoteCollectionTester(ItemCollectionTester, metaclass=ABCMeta): diff --git a/tests/remote/processors/check.py b/tests/shared/remote/processors/check.py similarity index 95% rename from tests/remote/processors/check.py rename to tests/shared/remote/processors/check.py index 405886b4..90b36e27 100644 --- a/tests/remote/processors/check.py +++ b/tests/shared/remote/processors/check.py @@ -6,14 +6,14 @@ import pytest from pytest_mock import MockerFixture -from syncify.abstract.object import BasicCollection +from syncify.shared.core.object import BasicCollection from syncify.local.track import LocalTrack -from syncify.remote.enums import RemoteObjectType -from syncify.remote.object import RemotePlaylist -from syncify.remote.processors.check import RemoteItemChecker -from tests.api.utils import path_token -from tests.local.utils import random_tracks, random_track -from tests.remote.utils import RemoteMock +from syncify.shared.remote.enums import RemoteObjectType +from syncify.shared.remote.object import RemotePlaylist +from syncify.shared.remote.processors.check import RemoteItemChecker +from tests.shared.api.utils import path_token +from tests.local.track.utils import random_track, random_tracks +from tests.shared.remote.utils import RemoteMock from tests.spotify.utils import random_uri, random_uris from tests.utils import random_str, get_stdout diff --git a/tests/remote/processors/search.py b/tests/shared/remote/processors/search.py similarity index 93% rename from tests/remote/processors/search.py rename to tests/shared/remote/processors/search.py index 249aec53..2f09bcc0 100644 --- a/tests/remote/processors/search.py +++ b/tests/shared/remote/processors/search.py @@ -5,16 +5,16 @@ import pytest -from syncify.abstract import Item -from syncify.abstract.collection import ItemCollection -from syncify.abstract.enums import TagFields as Tag -from syncify.abstract.object import BasicCollection, Album +from syncify.shared.core.base import Item +from syncify.shared.core.collection import ItemCollection +from syncify.shared.core.enums import TagFields as Tag +from syncify.shared.core.object import BasicCollection, Album from syncify.local.collection import LocalAlbum from syncify.local.track import LocalTrack -from syncify.remote.enums import RemoteObjectType -from syncify.remote.processors.search import RemoteItemSearcher, SearchSettings -from tests.local.utils import random_track, random_tracks -from tests.remote.utils import RemoteMock +from syncify.shared.remote.enums import RemoteObjectType +from syncify.shared.remote.processors.search import RemoteItemSearcher, SearchSettings +from tests.local.track.utils import random_track, random_tracks +from tests.shared.remote.utils import RemoteMock class RemoteItemSearcherTester(ABC): diff --git a/tests/remote/utils.py b/tests/shared/remote/utils.py similarity index 70% rename from tests/remote/utils.py rename to tests/shared/remote/utils.py index 2b8e1c18..87dd4d9d 100644 --- a/tests/remote/utils.py +++ b/tests/shared/remote/utils.py @@ -1,7 +1,6 @@ import re from abc import abstractmethod -from collections.abc import Iterable, Mapping -from random import choice, randrange +from collections.abc import Mapping from typing import Any from urllib.parse import parse_qs @@ -9,40 +8,12 @@ # noinspection PyProtectedMember,PyUnresolvedReferences from requests_mock.request import _RequestObjectProxy -from syncify.remote.enums import RemoteIDType, RemoteObjectType -from syncify.remote.processors.wrangle import RemoteDataWrangler -from tests.spotify.utils import random_id, random_ids -from tests.utils import random_str +from syncify.shared.remote.enums import RemoteIDType, RemoteObjectType ALL_ID_TYPES = RemoteIDType.all() ALL_ITEM_TYPES = RemoteObjectType.all() -def random_id_type(wrangler: RemoteDataWrangler, kind: RemoteObjectType, id_: str | None = None) -> str: - """Convert the given ``id_`` to a random ID type""" - type_in = RemoteIDType.ID - type_out = choice(ALL_ID_TYPES) - return wrangler.convert(id_ or random_id(), kind=kind, type_in=type_in, type_out=type_out) - - -def random_id_types( - wrangler: RemoteDataWrangler, - kind: RemoteObjectType, - id_list: Iterable[str] | None = None, - start: int = 1, - stop: int = 10 -) -> list[str]: - """Generate list of random ID types based on input item type""" - if id_list: - pass - elif kind == RemoteObjectType.USER: - id_list = [random_str(1, RemoteIDType.ID.value - 1) for _ in range(randrange(start=start, stop=stop))] - else: - id_list = random_ids(start=start, stop=stop) - - return [random_id_type(id_=id_, wrangler=wrangler, kind=kind) for id_ in id_list] - - class RemoteMock(Mocker): """Generates responses and sets up Remote API requests mock""" diff --git a/tests/test_fields.py b/tests/shared/test_fields.py similarity index 84% rename from tests/test_fields.py rename to tests/shared/test_fields.py index 1644c4ca..8e819d17 100644 --- a/tests/test_fields.py +++ b/tests/shared/test_fields.py @@ -1,10 +1,11 @@ import pytest -from syncify.abstract.object import Track, Playlist, Folder, Artist, Album -from syncify.fields import FolderField, PlaylistField, AlbumField, ArtistField -from syncify.fields import TrackField, LocalTrackField +from syncify.shared.core.object import Track, Playlist, Folder, Artist, Album +from syncify.shared.fields import FolderField, PlaylistField, AlbumField, ArtistField +from syncify.shared.fields import TrackField +from syncify.local.track.fields import LocalTrackField from syncify.local.track import LocalTrack -from tests.abstract.enums import FieldTester, TagFieldTester +from tests.shared.core.enums import FieldTester, TagFieldTester class TestTrackField(TagFieldTester): diff --git a/tests/utils/test_helpers.py b/tests/shared/test_helpers.py similarity index 93% rename from tests/utils/test_helpers.py rename to tests/shared/test_helpers.py index 2828f7d6..aad0e684 100644 --- a/tests/utils/test_helpers.py +++ b/tests/shared/test_helpers.py @@ -2,10 +2,10 @@ import pytest -from syncify.exception import SyncifyTypeError -from syncify.utils.helpers import flatten_nested, merge_maps, get_most_common_values -from syncify.utils.helpers import limit_value, to_collection, unique_list -from syncify.utils.helpers import strip_ignore_words, safe_format_map, get_max_width, align_and_truncate +from syncify.shared.exception import SyncifyTypeError +from syncify.shared.utils import flatten_nested, merge_maps, get_most_common_values +from syncify.shared.utils import limit_value, to_collection, unique_list +from syncify.shared.utils import strip_ignore_words, safe_format_map, get_max_width, align_and_truncate ########################################################################### diff --git a/tests/utils/test_logger.py b/tests/shared/test_logger.py similarity index 94% rename from tests/utils/test_logger.py rename to tests/shared/test_logger.py index 8c138ac2..01857f07 100644 --- a/tests/utils/test_logger.py +++ b/tests/shared/test_logger.py @@ -11,8 +11,8 @@ import pytest from syncify import PACKAGE_ROOT -from syncify.utils.logger import SyncifyLogger, INFO_EXTRA, REPORT, STAT, LOGGING_DT_FORMAT -from syncify.utils.logger import format_full_func_name, LogFileFilter, CurrentTimeRotatingFileHandler +from syncify.shared.logger import SyncifyLogger, INFO_EXTRA, REPORT, STAT, LOGGING_DT_FORMAT +from syncify.shared.logger import format_full_func_name, LogFileFilter, CurrentTimeRotatingFileHandler from tests.utils import random_str @@ -89,6 +89,7 @@ def test_get_progress_bar(logger: SyncifyLogger): assert bar in logger._bars handler.setLevel(logging.WARNING) + logger.disable_bars = False bar = logger.get_progress_bar( iterable=range(0, 50), initial=10, @@ -128,6 +129,8 @@ def test_get_progress_bar(logger: SyncifyLogger): assert bar.leave assert bar.pos == 0 + logger.disable_bars = True + def test_copy(logger: SyncifyLogger): assert id(copy(logger)) == id(logger) diff --git a/tests/spotify/api/mock.py b/tests/spotify/api/mock.py index 76f5ad8c..c5d6b715 100644 --- a/tests/spotify/api/mock.py +++ b/tests/spotify/api/mock.py @@ -14,11 +14,11 @@ # noinspection PyProtectedMember,PyUnresolvedReferences from requests_mock.response import _Context as Context -from syncify.abstract.enums import SyncifyEnum -from syncify.remote.enums import RemoteObjectType as ObjectType +from syncify.shared.core.enums import SyncifyEnum +from syncify.shared.remote.enums import RemoteObjectType as ObjectType from syncify.spotify import URL_API, URL_EXT, SPOTIFY_NAME from syncify.spotify.api import SpotifyAPI -from tests.remote.utils import RemoteMock +from tests.shared.remote.utils import RemoteMock from tests.spotify.utils import random_id from tests.utils import random_str, random_date_str, random_dt, random_genres diff --git a/tests/spotify/api/test_spotify_api_item.py b/tests/spotify/api/test_spotify_api_item.py index 7f38e45b..4bb87d24 100644 --- a/tests/spotify/api/test_spotify_api_item.py +++ b/tests/spotify/api/test_spotify_api_item.py @@ -9,13 +9,14 @@ # noinspection PyProtectedMember,PyUnresolvedReferences from requests_mock.request import _RequestObjectProxy as Request -from syncify.api.exception import APIError -from syncify.remote.enums import RemoteObjectType as ObjectType, RemoteIDType as IDType, RemoteIDType -from syncify.remote.exception import RemoteObjectTypeError +from syncify.shared.api.exception import APIError +from syncify.shared.remote.enums import RemoteObjectType as ObjectType, RemoteIDType as IDType, RemoteIDType +from syncify.shared.remote.exception import RemoteObjectTypeError from syncify.spotify.api import SpotifyAPI -from tests.remote.utils import random_id_type, random_id_types, ALL_ITEM_TYPES +from tests.shared.remote.utils import ALL_ITEM_TYPES from tests.spotify.api.mock import SpotifyMock, idfn -from tests.spotify.utils import random_ids, random_id, random_uri, random_api_url, random_ext_url +from tests.spotify.utils import random_ids, random_id, random_id_type, random_id_types +from tests.spotify.utils import random_uri, random_api_url, random_ext_url from tests.utils import random_str diff --git a/tests/spotify/api/test_spotify_api_core.py b/tests/spotify/api/test_spotify_api_misc.py similarity index 96% rename from tests/spotify/api/test_spotify_api_core.py rename to tests/spotify/api/test_spotify_api_misc.py index 284483e3..cf777e0c 100644 --- a/tests/spotify/api/test_spotify_api_core.py +++ b/tests/spotify/api/test_spotify_api_misc.py @@ -4,7 +4,7 @@ import pytest -from syncify.remote.enums import RemoteObjectType as ObjectType +from syncify.shared.remote.enums import RemoteObjectType as ObjectType from syncify.spotify import URL_EXT from syncify.spotify.api import SpotifyAPI from tests.spotify.api.mock import SpotifyMock, idfn diff --git a/tests/spotify/api/test_spotify_api_playlist.py b/tests/spotify/api/test_spotify_api_playlist.py index 4e50b20f..14f064ca 100644 --- a/tests/spotify/api/test_spotify_api_playlist.py +++ b/tests/spotify/api/test_spotify_api_playlist.py @@ -5,12 +5,13 @@ import pytest from syncify import PROGRAM_NAME -from syncify.remote.enums import RemoteObjectType as ObjectType, RemoteIDType -from syncify.remote.exception import RemoteObjectTypeError, RemoteIDTypeError +from syncify.shared.remote.enums import RemoteObjectType as ObjectType, RemoteIDType +from syncify.shared.remote.exception import RemoteObjectTypeError, RemoteIDTypeError from syncify.spotify.api import SpotifyAPI -from tests.remote.utils import random_id_type, random_id_types, ALL_ITEM_TYPES +from tests.shared.remote.utils import ALL_ITEM_TYPES from tests.spotify.api.mock import SpotifyMock -from tests.spotify.utils import random_id, random_ids, random_uris, random_api_urls, random_ext_urls +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 class TestSpotifyAPIPlaylists: diff --git a/tests/spotify/library/utils.py b/tests/spotify/library/utils.py deleted file mode 100644 index 0405ca8d..00000000 --- a/tests/spotify/library/utils.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Any - -from syncify.spotify.base import SpotifyObject - - -def assert_id_attributes(item: SpotifyObject, response: dict[str, Any]): - """Check a given :py:class:`SpotifyObject` has the expected attributes relating to its identification""" - assert item.has_uri - assert item.uri == response["uri"] - assert item.id == response["id"] - assert item.url == response["href"] - assert item.url_ext == response["external_urls"]["spotify"] diff --git a/tests/spotify/library/test_spotify_collection.py b/tests/spotify/test_spotify_collection.py similarity index 78% rename from tests/spotify/library/test_spotify_collection.py rename to tests/spotify/test_spotify_collection.py index 6b1a3dc5..87676072 100644 --- a/tests/spotify/library/test_spotify_collection.py +++ b/tests/spotify/test_spotify_collection.py @@ -1,106 +1,22 @@ -from abc import ABCMeta, abstractmethod from collections.abc import Iterable from copy import deepcopy from datetime import date from random import randrange from typing import Any -from urllib.parse import parse_qs import pytest -from syncify.api.exception import APIError -from syncify.remote.enums import RemoteObjectType -from syncify.remote.exception import RemoteObjectTypeError, RemoteError +from tests.spotify.testers import SpotifyCollectionLoaderTester +from syncify.shared.api.exception import APIError +from syncify.shared.remote.enums import RemoteObjectType +from syncify.shared.remote.exception import RemoteObjectTypeError, RemoteError from syncify.spotify import URL_API from syncify.spotify.api import SpotifyAPI -from syncify.spotify.base import SpotifyItem from syncify.spotify.object import SpotifyAlbum, SpotifyArtist -from syncify.spotify.object import SpotifyTrack, SpotifyCollectionLoader -from tests.remote.library.object import RemoteCollectionTester +from syncify.spotify.object import SpotifyTrack +from tests.shared.remote.object import RemoteCollectionTester from tests.spotify.api.mock import SpotifyMock -from tests.spotify.library.utils import assert_id_attributes - - -class SpotifyCollectionLoaderTester(RemoteCollectionTester, metaclass=ABCMeta): - - @abstractmethod - def collection_merge_items(self, *args, **kwargs) -> Iterable[SpotifyItem]: - raise NotImplementedError - - @staticmethod - def test_load_without_items( - collection: SpotifyCollectionLoader, - response_valid: dict[str, Any], - api: SpotifyAPI, - api_mock: SpotifyMock - ): - api_mock.reset_mock() # test checks the number of requests made - - unit = collection.__class__.__name__.removeprefix("Spotify") - kind = RemoteObjectType.from_name(unit)[0] - key = api.collection_item_map[kind].name.casefold() + "s" - - test = collection.__class__.load(response_valid["href"], api=api, extend_tracks=True) - - assert test.name == response_valid["name"] - assert test.id == response_valid["id"] - assert test.url == response_valid["href"] - - requests = api_mock.get_requests(test.url) - requests += api_mock.get_requests(f"{test.url}/{key}") - requests += api_mock.get_requests(f"{collection.api.api_url_base}/audio-features") - - # 1 call for initial collection + (pages - 1) for tracks + (pages) for audio-features - assert len(requests) == 2 * api_mock.calculate_pages_from_response(test.response) - - # input items given, but no key to search on still loads - test = collection.__class__.load(response_valid, api=api, items=response_valid.pop(key), extend_tracks=True) - - assert test.name == response_valid["name"] - assert test.id == response_valid["id"] - assert test.url == response_valid["href"] - - @staticmethod - def assert_load_with_tracks( - cls: type[SpotifyCollectionLoader], - items: list[SpotifyTrack], - response: dict[str, Any], - api: SpotifyAPI, - api_mock: SpotifyMock, - ): - """Run test with assertions on load method with given ``items``""" - unit = cls.__name__.removeprefix("Spotify") - kind = RemoteObjectType.from_name(unit)[0] - key = api.collection_item_map[kind].name.casefold() + "s" - - test = cls.load(response, api=api, items=items, extend_tracks=True) - assert len(test.response[key]["items"]) == response[key]["total"] - assert len(test.items) == response[key]["total"] - assert not api_mock.get_requests(test.url) # playlist URL was not called - - # requests to extend album start from page 2 onward - requests = api_mock.get_requests(test.url) - requests += api_mock.get_requests(f"{test.url}/{key}") - requests += api_mock.get_requests(f"{api.api_url_base}/audio-features") - - # 0 calls for initial collection + (extend_pages - 1) for tracks + (extend_pages) for audio-features - # + (get_pages) for audio-features get on response items not in input items - if kind == RemoteObjectType.PLAYLIST: - input_ids = {item["track"]["id"] for item in response["tracks"]["items"]} - {item.id for item in items} - else: - input_ids = {item["id"] for item in response["tracks"]["items"]} - {item.id for item in items} - get_pages = api_mock.calculate_pages(limit=test.response[key]["limit"], total=len(input_ids)) - extend_pages = api_mock.calculate_pages_from_response(test.response) - assert len(requests) == 2 * extend_pages - 1 + get_pages - - # ensure none of the items ids were requested - input_ids = {item.id for item in items} - for request in api_mock.get_requests(f"{test.url}/{key}"): - params = parse_qs(request.query) - if "ids" not in params: - continue - - assert not input_ids.intersection(params["ids"][0].split(",")) +from tests.spotify.utils import assert_id_attributes class TestSpotifyAlbum(SpotifyCollectionLoaderTester): diff --git a/tests/spotify/library/test_spotify_library.py b/tests/spotify/test_spotify_library.py similarity index 96% rename from tests/spotify/library/test_spotify_library.py rename to tests/spotify/test_spotify_library.py index eadf356c..c2504059 100644 --- a/tests/spotify/library/test_spotify_library.py +++ b/tests/spotify/test_spotify_library.py @@ -4,11 +4,11 @@ import pytest -from syncify.remote.enums import RemoteObjectType +from syncify.shared.remote.enums import RemoteObjectType from syncify.spotify.api import SpotifyAPI from syncify.spotify.library import SpotifyLibrary from syncify.spotify.object import SpotifyTrack -from tests.remote.library.library import RemoteLibraryTester +from tests.shared.remote.library import RemoteLibraryTester from tests.spotify.api.mock import SpotifyMock diff --git a/tests/spotify/library/test_spotify_playlist.py b/tests/spotify/test_spotify_playlist.py similarity index 94% rename from tests/spotify/library/test_spotify_playlist.py rename to tests/spotify/test_spotify_playlist.py index 57fdf648..12157552 100644 --- a/tests/spotify/library/test_spotify_playlist.py +++ b/tests/spotify/test_spotify_playlist.py @@ -6,19 +6,18 @@ import pytest +from tests.spotify.testers import SpotifyCollectionLoaderTester from syncify import PROGRAM_NAME -from syncify.api.exception import APIError -from syncify.remote.enums import RemoteObjectType -from syncify.remote.exception import RemoteObjectTypeError, RemoteError +from syncify.shared.api.exception import APIError +from syncify.shared.remote.enums import RemoteObjectType +from syncify.shared.remote.exception import RemoteObjectTypeError, RemoteError from syncify.spotify.api import SpotifyAPI from syncify.spotify.exception import SpotifyCollectionError from syncify.spotify.object import SpotifyPlaylist from syncify.spotify.object import SpotifyTrack -from tests.remote.library.object import RemotePlaylistTester +from tests.shared.remote.object import RemotePlaylistTester from tests.spotify.api.mock import SpotifyMock -from tests.spotify.library.test_spotify_collection import SpotifyCollectionLoaderTester -from tests.spotify.library.utils import assert_id_attributes -from tests.spotify.utils import random_uri +from tests.spotify.utils import random_uri, assert_id_attributes class TestSpotifyPlaylist(SpotifyCollectionLoaderTester, RemotePlaylistTester): diff --git a/tests/spotify/test_spotify_processors.py b/tests/spotify/test_spotify_processors.py index 890ad57c..18acb2aa 100644 --- a/tests/spotify/test_spotify_processors.py +++ b/tests/spotify/test_spotify_processors.py @@ -2,22 +2,22 @@ import pytest -from syncify.abstract.enums import TagFields as Tag -from syncify.exception import SyncifyEnumError +from syncify.shared.core.enums import TagFields as Tag +from syncify.shared.exception import SyncifyEnumError from syncify.local.collection import LocalAlbum from syncify.local.track import LocalTrack from syncify.processors.match import CleanTagConfig -from syncify.remote.enums import RemoteIDType as IDType, RemoteObjectType as ObjectType -from syncify.remote.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError -from syncify.remote.processors.search import SearchSettings +from syncify.shared.remote.enums import RemoteIDType as IDType, RemoteObjectType as ObjectType +from syncify.shared.remote.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError +from syncify.shared.remote.processors.search import SearchSettings from syncify.spotify import URL_API, URL_EXT from syncify.spotify.api import SpotifyAPI from syncify.spotify.object import SpotifyTrack, SpotifyAlbum from syncify.spotify.processors.processors import SpotifyItemSearcher, SpotifyItemChecker from syncify.spotify.processors.wrangle import SpotifyDataWrangler -from tests.local.utils import random_track -from tests.remote.processors.check import RemoteItemCheckerTester -from tests.remote.processors.search import RemoteItemSearcherTester +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 tests.utils import random_str diff --git a/tests/spotify/library/test_spotify_track.py b/tests/spotify/test_spotify_track.py similarity index 95% rename from tests/spotify/library/test_spotify_track.py rename to tests/spotify/test_spotify_track.py index 69b955df..a3aba970 100644 --- a/tests/spotify/library/test_spotify_track.py +++ b/tests/spotify/test_spotify_track.py @@ -5,13 +5,13 @@ import pytest -from syncify.api.exception import APIError -from syncify.remote.exception import RemoteObjectTypeError +from syncify.shared.api.exception import APIError +from syncify.shared.remote.exception import RemoteObjectTypeError from syncify.spotify.api import SpotifyAPI from syncify.spotify.object import SpotifyTrack -from tests.abstract.base import ItemTester +from tests.shared.core.base import ItemTester from tests.spotify.api.mock import SpotifyMock -from tests.spotify.library.utils import assert_id_attributes +from tests.spotify.utils import assert_id_attributes class TestSpotifyTrack(ItemTester): diff --git a/tests/spotify/testers.py b/tests/spotify/testers.py new file mode 100644 index 00000000..64b023de --- /dev/null +++ b/tests/spotify/testers.py @@ -0,0 +1,96 @@ +from abc import ABCMeta, abstractmethod +from collections.abc import Iterable +from typing import Any +from urllib.parse import parse_qs + +# noinspection PyProtectedMember,PyUnresolvedReferences +from requests_mock.request import _RequestObjectProxy + +from syncify.shared.remote.enums import RemoteObjectType +from syncify.spotify.api import SpotifyAPI +from syncify.spotify.base import SpotifyItem +from syncify.spotify.object import SpotifyCollectionLoader, SpotifyTrack +from tests.shared.remote.object import RemoteCollectionTester +from tests.spotify.api.mock import SpotifyMock + + +class SpotifyCollectionLoaderTester(RemoteCollectionTester, metaclass=ABCMeta): + + @abstractmethod + def collection_merge_items(self, *args, **kwargs) -> Iterable[SpotifyItem]: + raise NotImplementedError + + @staticmethod + def test_load_without_items( + collection: SpotifyCollectionLoader, + response_valid: dict[str, Any], + api: SpotifyAPI, + api_mock: SpotifyMock + ): + api_mock.reset_mock() # test checks the number of requests made + + unit = collection.__class__.__name__.removeprefix("Spotify") + kind = RemoteObjectType.from_name(unit)[0] + key = api.collection_item_map[kind].name.casefold() + "s" + + test = collection.__class__.load(response_valid["href"], api=api, extend_tracks=True) + + assert test.name == response_valid["name"] + assert test.id == response_valid["id"] + assert test.url == response_valid["href"] + + requests = api_mock.get_requests(test.url) + requests += api_mock.get_requests(f"{test.url}/{key}") + requests += api_mock.get_requests(f"{collection.api.api_url_base}/audio-features") + + # 1 call for initial collection + (pages - 1) for tracks + (pages) for audio-features + assert len(requests) == 2 * api_mock.calculate_pages_from_response(test.response) + + # input items given, but no key to search on still loads + test = collection.__class__.load(response_valid, api=api, items=response_valid.pop(key), extend_tracks=True) + + assert test.name == response_valid["name"] + assert test.id == response_valid["id"] + assert test.url == response_valid["href"] + + @staticmethod + def assert_load_with_tracks( + cls: type[SpotifyCollectionLoader], + items: list[SpotifyTrack], + response: dict[str, Any], + api: SpotifyAPI, + api_mock: SpotifyMock, + ): + """Run test with assertions on load method with given ``items``""" + unit = cls.__name__.removeprefix("Spotify") + kind = RemoteObjectType.from_name(unit)[0] + key = api.collection_item_map[kind].name.casefold() + "s" + + test = cls.load(response, api=api, items=items, extend_tracks=True) + assert len(test.response[key]["items"]) == response[key]["total"] + assert len(test.items) == response[key]["total"] + assert not api_mock.get_requests(test.url) # playlist URL was not called + + # requests to extend album start from page 2 onward + requests = api_mock.get_requests(test.url) + requests += api_mock.get_requests(f"{test.url}/{key}") + requests += api_mock.get_requests(f"{api.api_url_base}/audio-features") + + # 0 calls for initial collection + (extend_pages - 1) for tracks + (extend_pages) for audio-features + # + (get_pages) for audio-features get on response items not in input items + if kind == RemoteObjectType.PLAYLIST: + input_ids = {item["track"]["id"] for item in response["tracks"]["items"]} - {item.id for item in items} + else: + input_ids = {item["id"] for item in response["tracks"]["items"]} - {item.id for item in items} + get_pages = api_mock.calculate_pages(limit=test.response[key]["limit"], total=len(input_ids)) + extend_pages = api_mock.calculate_pages_from_response(test.response) + assert len(requests) == 2 * extend_pages - 1 + get_pages + + # ensure none of the items ids were requested + input_ids = {item.id for item in items} + for request in api_mock.get_requests(f"{test.url}/{key}"): + params = parse_qs(request.query) + if "ids" not in params: + continue + + assert not input_ids.intersection(params["ids"][0].split(",")) diff --git a/tests/spotify/utils.py b/tests/spotify/utils.py index f0dc43e9..c03d350a 100644 --- a/tests/spotify/utils.py +++ b/tests/spotify/utils.py @@ -1,9 +1,19 @@ -from random import randrange +from collections.abc import Iterable +from random import choice, randrange +from typing import Any -from syncify.remote.enums import RemoteObjectType, RemoteIDType +# noinspection PyProtectedMember,PyUnresolvedReferences +from requests_mock.request import _RequestObjectProxy + +from syncify.shared.remote.enums import RemoteIDType, RemoteObjectType +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler from syncify.spotify import URL_API, URL_EXT, SPOTIFY_NAME +from syncify.spotify.base import SpotifyObject from tests.utils import random_str +ALL_ID_TYPES = RemoteIDType.all() +ALL_ITEM_TYPES = RemoteObjectType.all() + def random_id() -> str: """Generates a valid looking random Spotify ID""" @@ -16,6 +26,31 @@ def random_ids(start: int = 1, stop: int = 50) -> list[str]: return [random_id() for _ in range(range_)] +def random_id_type(wrangler: RemoteDataWrangler, kind: RemoteObjectType, id_: str | None = None) -> str: + """Convert the given ``id_`` to a random ID type""" + type_in = RemoteIDType.ID + type_out = choice(ALL_ID_TYPES) + return wrangler.convert(id_ or random_id(), kind=kind, type_in=type_in, type_out=type_out) + + +def random_id_types( + wrangler: RemoteDataWrangler, + kind: RemoteObjectType, + id_list: Iterable[str] | None = None, + start: int = 1, + stop: int = 10 +) -> list[str]: + """Generate list of random ID types based on input item type""" + if id_list: + pass + elif kind == RemoteObjectType.USER: + id_list = [random_str(1, RemoteIDType.ID.value - 1) for _ in range(randrange(start=start, stop=stop))] + else: + id_list = random_ids(start=start, stop=stop) + + return [random_id_type(id_=id_, wrangler=wrangler, kind=kind) for id_ in id_list] + + def random_uri(kind: RemoteObjectType = RemoteObjectType.TRACK) -> str: """Generates a valid looking random Spotify URI of item :py:class:`RemoteObjectType` ``kind``""" return f"{SPOTIFY_NAME.lower()}:{kind.name.lower()}:{random_id()}" @@ -47,3 +82,12 @@ def random_ext_urls(kind: RemoteObjectType = RemoteObjectType.TRACK, start: int """Generates many valid looking random Spotify external URLs of item :py:class:`RemoteObjectType` ``kind``""" range_ = randrange(start=start, stop=stop) if start < stop else start return [random_ext_url(kind=kind) for _ in range(range_)] + + +def assert_id_attributes(item: SpotifyObject, response: dict[str, Any]): + """Check a given :py:class:`SpotifyObject` has the expected attributes relating to its identification""" + assert item.has_uri + assert item.uri == response["uri"] + assert item.id == response["id"] + assert item.url == response["href"] + assert item.url_ext == response["external_urls"]["spotify"] diff --git a/tests/test_config.py b/tests/test_config.py index 54f22196..442b1276 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,17 +7,17 @@ from requests_cache import CachedSession from syncify import PACKAGE_ROOT, MODULE_ROOT -from syncify.abstract.enums import TagFields +from syncify.shared.core.enums import TagFields from syncify.config import ConfigLocal, ConfigMusicBee from syncify.config import ConfigRemote, ConfigSpotify from syncify.config import LOCAL_CONFIG, REMOTE_CONFIG, Config, ConfigFilter, ConfigReports -from syncify.exception import ConfigError, SyncifyError -from syncify.fields import LocalTrackField +from syncify.shared.exception import ConfigError, SyncifyError +from syncify.local.track.fields import LocalTrackField from syncify.local.exception import FileDoesNotExistError -from syncify.remote.processors.wrangle import RemoteDataWrangler -from syncify.utils.helpers import correct_platform_separators -from syncify.utils.logger import SyncifyLogger -from tests.abstract.misc import PrettyPrinterTester +from syncify.shared.remote.processors.wrangle import RemoteDataWrangler +from syncify.shared.utils import correct_platform_separators +from syncify.shared.logger import SyncifyLogger +from tests.shared.core.misc import PrettyPrinterTester from tests.utils import path_resources, path_txt path_config = join(path_resources, "test_config.yml") @@ -62,7 +62,6 @@ def test_load_log_config(self, config_empty: Config, tmp_path: str): loggers = [logger.name for logger in logging.getLogger().getChildren()] assert "__main__" not in loggers - assert MODULE_ROOT not in loggers config_empty.load_log_config(path_logging, "test", "__main__") diff --git a/tests/test_report.py b/tests/test_report.py index 3eb4abea..3ec8d4aa 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -5,7 +5,7 @@ import pytest -from syncify.fields import LocalTrackField +from syncify.local.track.fields import LocalTrackField from syncify.local.library import LocalLibrary from syncify.local.playlist import M3U from syncify.report import report_playlist_differences, report_missing_tags @@ -13,7 +13,7 @@ from syncify.spotify.library import SpotifyLibrary from syncify.spotify.object import SpotifyPlaylist from syncify.spotify.processors.wrangle import SpotifyDataWrangler -from tests.local.utils import random_track +from tests.local.track.utils import random_track from tests.spotify.api.mock import SpotifyMock from tests.spotify.utils import random_uri