From 110cd65a79995224e41235b26cf05bcd5ec1cf99 Mon Sep 17 00:00:00 2001 From: geo-martino Date: Tue, 11 Jun 2024 14:35:34 -0400 Subject: [PATCH 1/4] remove remote.core.processors package --- docs/_howto/scripts/local.playlist.load-save/p3_wrangler.py | 2 +- docs/_howto/scripts/local.track.load-save/p1_wrangler.py | 2 +- docs/_howto/scripts/sync/p1.py | 6 ++---- docs/release-history.rst | 4 ++++ musify/libraries/local/collection.py | 2 +- musify/libraries/local/library/library.py | 2 +- musify/libraries/local/library/musicbee.py | 2 +- musify/libraries/local/playlist/base.py | 2 +- musify/libraries/local/playlist/m3u.py | 2 +- musify/libraries/local/playlist/utils.py | 2 +- musify/libraries/local/playlist/xautopf.py | 2 +- musify/libraries/local/track/_tags/base.py | 2 +- musify/libraries/local/track/track.py | 2 +- musify/libraries/local/track/utils.py | 2 +- musify/libraries/remote/core/api.py | 2 +- musify/libraries/remote/core/processors/__init__.py | 6 ------ musify/libraries/remote/core/{processors => }/wrangle.py | 0 musify/libraries/remote/spotify/api/api.py | 2 +- musify/libraries/remote/spotify/api/cache.py | 2 +- .../libraries/remote/spotify/{processors.py => wrangle.py} | 2 +- musify/{libraries/remote/core => }/processors/check.py | 2 +- musify/{libraries/remote/core => }/processors/search.py | 0 readme.py | 2 +- tests/conftest.py | 2 +- tests/libraries/local/conftest.py | 4 ++-- tests/libraries/local/library/test_musicbee.py | 2 +- tests/libraries/local/utils.py | 2 +- tests/libraries/remote/core/processors/check.py | 2 +- tests/libraries/remote/core/processors/search.py | 2 +- tests/libraries/remote/spotify/api/mock.py | 2 +- tests/libraries/remote/spotify/conftest.py | 2 +- tests/libraries/remote/spotify/test_processors.py | 6 +++--- tests/libraries/remote/spotify/utils.py | 4 ++-- tests/test_report.py | 2 +- 34 files changed, 39 insertions(+), 43 deletions(-) delete mode 100644 musify/libraries/remote/core/processors/__init__.py rename musify/libraries/remote/core/{processors => }/wrangle.py (100%) rename musify/libraries/remote/spotify/{processors.py => wrangle.py} (96%) rename musify/{libraries/remote/core => }/processors/check.py (97%) rename musify/{libraries/remote/core => }/processors/search.py (100%) diff --git a/docs/_howto/scripts/local.playlist.load-save/p3_wrangler.py b/docs/_howto/scripts/local.playlist.load-save/p3_wrangler.py index 133bd11d..220debef 100644 --- a/docs/_howto/scripts/local.playlist.load-save/p3_wrangler.py +++ b/docs/_howto/scripts/local.playlist.load-save/p3_wrangler.py @@ -1,5 +1,5 @@ from p3 import * -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler playlist = asyncio.run(load_playlist("", remote_wrangler=SpotifyDataWrangler())) diff --git a/docs/_howto/scripts/local.track.load-save/p1_wrangler.py b/docs/_howto/scripts/local.track.load-save/p1_wrangler.py index 28a74b8e..89f509f3 100644 --- a/docs/_howto/scripts/local.track.load-save/p1_wrangler.py +++ b/docs/_howto/scripts/local.track.load-save/p1_wrangler.py @@ -1,5 +1,5 @@ from p1_load import * -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler track = asyncio.run(load_track("", remote_wrangler=SpotifyDataWrangler())) diff --git a/docs/_howto/scripts/sync/p1.py b/docs/_howto/scripts/sync/p1.py index da6cb9ff..02197d44 100644 --- a/docs/_howto/scripts/sync/p1.py +++ b/docs/_howto/scripts/sync/p1.py @@ -1,11 +1,9 @@ -from p0 import * - from collections.abc import Collection from musify.libraries.core.collection import MusifyCollection from musify.libraries.remote.core.factory import RemoteObjectFactory -from musify.libraries.remote.core.processors.search import RemoteItemSearcher -from musify.libraries.remote.core.processors.check import RemoteItemChecker +from musify.processors.search import RemoteItemSearcher +from musify.processors.check import RemoteItemChecker from musify.processors.match import ItemMatcher diff --git a/docs/release-history.rst b/docs/release-history.rst index f951844e..7db8976d 100644 --- a/docs/release-history.rst +++ b/docs/release-history.rst @@ -100,6 +100,10 @@ Changed * :py:class:`.RemoteItemChecker` also uses the new :py:meth:`.RemoteAPI.follow_playlist` method when creating playlists to ensure that a user is following the playlists it creates to avoid 'ghost playlist' issue. * :py:meth:`.SpotifyAPI.create_playlist` now returns the full response rather than just the URL of the playlist. +* Moved :py:class:`.RemoteItemChecker` and :py:class:`.RemoteItemSearcher` to `musify.processors` package. +* Moved :py:class:`.RemoteDataWrangler` up a level to `musify.libraries.remote.core`. +* Deleted `musify.libraries.remote.core.processors` package. +* Renamed `musify.libraries.remote.spotify.processors` module to `musify.libraries.remote.spotify.wrangle`. Fixed ----- diff --git a/musify/libraries/local/collection.py b/musify/libraries/local/collection.py index 1fed0f4b..2fcb7916 100644 --- a/musify/libraries/local/collection.py +++ b/musify/libraries/local/collection.py @@ -20,7 +20,7 @@ from musify.libraries.local.exception import LocalCollectionError from musify.libraries.local.track import LocalTrack, SyncResultTrack, load_track, TRACK_FILETYPES from musify.libraries.local.track.field import LocalTrackField -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.log.logger import MusifyLogger from musify.types import UnitCollection, UnitIterable from musify.utils import get_most_common_values, to_collection, align_string, get_max_width diff --git a/musify/libraries/local/library/library.py b/musify/libraries/local/library/library.py index e8453344..b3e55830 100644 --- a/musify/libraries/local/library/library.py +++ b/musify/libraries/local/library/library.py @@ -17,7 +17,7 @@ from musify.libraries.local.playlist import PLAYLIST_CLASSES, LocalPlaylist, load_playlist from musify.libraries.local.track import TRACK_CLASSES, LocalTrack, load_track from musify.libraries.local.track.field import LocalTrackField -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.log import STAT from musify.processors.base import Filter from musify.processors.filter import FilterDefinedList diff --git a/musify/libraries/local/library/musicbee.py b/musify/libraries/local/library/musicbee.py index ec8b55cc..4da5d63a 100644 --- a/musify/libraries/local/library/musicbee.py +++ b/musify/libraries/local/library/musicbee.py @@ -18,7 +18,7 @@ from musify.libraries.local.library.library import LocalLibrary from musify.libraries.local.playlist import LocalPlaylist from musify.libraries.local.track import LocalTrack -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.processors.base import Filter from musify.types import Number from musify.utils import to_collection, required_modules_installed diff --git a/musify/libraries/local/playlist/base.py b/musify/libraries/local/playlist/base.py index a2c0017b..f25426a8 100644 --- a/musify/libraries/local/playlist/base.py +++ b/musify/libraries/local/playlist/base.py @@ -12,7 +12,7 @@ from musify.libraries.core.object import Playlist from musify.libraries.local.collection import LocalCollection from musify.libraries.local.track import LocalTrack -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.processors.base import Filter from musify.processors.limit import ItemLimiter from musify.processors.sort import ItemSorter diff --git a/musify/libraries/local/playlist/m3u.py b/musify/libraries/local/playlist/m3u.py index 948cc677..18329521 100644 --- a/musify/libraries/local/playlist/m3u.py +++ b/musify/libraries/local/playlist/m3u.py @@ -13,7 +13,7 @@ from musify.file.path_mapper import PathMapper from musify.libraries.local.playlist.base import LocalPlaylist from musify.libraries.local.track import LocalTrack, load_track -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.processors.filter import FilterDefinedList diff --git a/musify/libraries/local/playlist/utils.py b/musify/libraries/local/playlist/utils.py index a7c137b4..f92cf64a 100644 --- a/musify/libraries/local/playlist/utils.py +++ b/musify/libraries/local/playlist/utils.py @@ -13,7 +13,7 @@ from musify.libraries.local.playlist.m3u import M3U from musify.libraries.local.playlist.xautopf import XAutoPF, REQUIRED_MODULES as REQUIRED_XAUTOPF_MODULES from musify.libraries.local.track import LocalTrack -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.utils import required_modules_installed _playlist_classes = {M3U} diff --git a/musify/libraries/local/playlist/xautopf.py b/musify/libraries/local/playlist/xautopf.py index 0377d206..62da0597 100644 --- a/musify/libraries/local/playlist/xautopf.py +++ b/musify/libraries/local/playlist/xautopf.py @@ -16,7 +16,7 @@ from musify.file.path_mapper import PathMapper from musify.libraries.local.playlist.base import LocalPlaylist from musify.libraries.local.track import LocalTrack -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.processors.compare import Comparer from musify.processors.exception import SorterProcessorError from musify.processors.filter import FilterDefinedList, FilterComparers diff --git a/musify/libraries/local/track/_tags/base.py b/musify/libraries/local/track/_tags/base.py index 634debad..2e402119 100644 --- a/musify/libraries/local/track/_tags/base.py +++ b/musify/libraries/local/track/_tags/base.py @@ -7,7 +7,7 @@ from musify.core.enum import TagMap from musify.libraries.local.track.field import LocalTrackField -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler class TagProcessor[T: mutagen.FileType](metaclass=ABCMeta): diff --git a/musify/libraries/local/track/track.py b/musify/libraries/local/track/track.py index 834ee113..09b3b302 100644 --- a/musify/libraries/local/track/track.py +++ b/musify/libraries/local/track/track.py @@ -22,7 +22,7 @@ from musify.libraries.local.track.field import LocalTrackField as Tags # noinspection PyProtectedMember from musify.libraries.local.track._tags import TagReader, TagWriter, SyncResultTrack -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.types import UnitIterable from musify.utils import to_collection diff --git a/musify/libraries/local/track/utils.py b/musify/libraries/local/track/utils.py index 8dc39f6f..a5b30882 100644 --- a/musify/libraries/local/track/utils.py +++ b/musify/libraries/local/track/utils.py @@ -12,7 +12,7 @@ from musify.libraries.local.track.m4a import M4A from musify.libraries.local.track.mp3 import MP3 from musify.libraries.local.track.wma import WMA -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler TRACK_CLASSES = frozenset({FLAC, MP3, M4A, WMA}) TRACK_FILETYPES = frozenset(filetype for c in TRACK_CLASSES for filetype in c.valid_extensions) diff --git a/musify/libraries/remote/core/api.py b/musify/libraries/remote/core/api.py index 182e74b2..de80b08d 100644 --- a/musify/libraries/remote/core/api.py +++ b/musify/libraries/remote/core/api.py @@ -16,7 +16,7 @@ from musify.api.request import RequestHandler from musify.libraries.remote.core import RemoteResponse from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.libraries.remote.core.types import APIInputValueSingle, APIInputValueMulti from musify.log.logger import MusifyLogger from musify.types import UnitSequence, JSON, UnitList diff --git a/musify/libraries/remote/core/processors/__init__.py b/musify/libraries/remote/core/processors/__init__.py deleted file mode 100644 index 4ef62073..00000000 --- a/musify/libraries/remote/core/processors/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Generic processors relating to remote operations. - -These can be as simple as a wrangler which converts between ID types for a remote object, -to more complex operations/algorithms such as search+match algorithms. -""" diff --git a/musify/libraries/remote/core/processors/wrangle.py b/musify/libraries/remote/core/wrangle.py similarity index 100% rename from musify/libraries/remote/core/processors/wrangle.py rename to musify/libraries/remote/core/wrangle.py diff --git a/musify/libraries/remote/spotify/api/api.py b/musify/libraries/remote/spotify/api/api.py index eb8eb620..a38a3741 100644 --- a/musify/libraries/remote/spotify/api/api.py +++ b/musify/libraries/remote/spotify/api/api.py @@ -18,7 +18,7 @@ from musify.libraries.remote.spotify.api.item import SpotifyAPIItems from musify.libraries.remote.spotify.api.misc import SpotifyAPIMisc from musify.libraries.remote.spotify.api.playlist import SpotifyAPIPlaylists -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler from musify.utils import safe_format_map, merge_maps URL_AUTH = "https://accounts.spotify.com" diff --git a/musify/libraries/remote/spotify/api/cache.py b/musify/libraries/remote/spotify/api/cache.py index 04f932a2..3b086794 100644 --- a/musify/libraries/remote/spotify/api/cache.py +++ b/musify/libraries/remote/spotify/api/cache.py @@ -5,7 +5,7 @@ from musify.api.cache.backend.base import RequestSettings from musify.libraries.remote.core.enum import RemoteIDType from musify.libraries.remote.core.exception import RemoteObjectTypeError -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler class SpotifyRequestSettings(RequestSettings): diff --git a/musify/libraries/remote/spotify/processors.py b/musify/libraries/remote/spotify/wrangle.py similarity index 96% rename from musify/libraries/remote/spotify/processors.py rename to musify/libraries/remote/spotify/wrangle.py index e87c09ca..552a8fcd 100644 --- a/musify/libraries/remote/spotify/processors.py +++ b/musify/libraries/remote/spotify/wrangle.py @@ -12,7 +12,7 @@ from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType from musify.libraries.remote.core.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError from musify.libraries.remote.core.types import APIInputValueSingle, APIInputValueMulti -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.libraries.remote.spotify import SOURCE_NAME from musify.utils import to_collection diff --git a/musify/libraries/remote/core/processors/check.py b/musify/processors/check.py similarity index 97% rename from musify/libraries/remote/core/processors/check.py rename to musify/processors/check.py index db394433..03c94c49 100644 --- a/musify/libraries/remote/core/processors/check.py +++ b/musify/processors/check.py @@ -21,7 +21,7 @@ from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType from musify.libraries.remote.core.factory import RemoteObjectFactory from musify.libraries.remote.core.object import RemotePlaylist -from musify.libraries.remote.core.processors.search import RemoteItemSearcher +from musify.processors.search import RemoteItemSearcher from musify.log import REPORT from musify.log.logger import MusifyLogger from musify.processors.base import InputProcessor diff --git a/musify/libraries/remote/core/processors/search.py b/musify/processors/search.py similarity index 100% rename from musify/libraries/remote/core/processors/search.py rename to musify/processors/search.py diff --git a/readme.py b/readme.py index b9b84114..3dd785b8 100644 --- a/readme.py +++ b/readme.py @@ -6,7 +6,7 @@ from musify.libraries.local.playlist import PLAYLIST_FILETYPES from musify.libraries.local.library import LIBRARY_CLASSES, LocalLibrary from musify.utils import SafeDict -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler SRC_FILENAME = "README.template.md" TRG_FILENAME = SRC_FILENAME.replace(".template", "") diff --git a/tests/conftest.py b/tests/conftest.py index fb89eabd..e862827b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,7 @@ from musify import MODULE_ROOT from musify.libraries.remote.core.enum import RemoteObjectType from musify.libraries.remote.spotify.api import SpotifyAPI -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler from musify.log.logger import MusifyLogger from musify.types import UnitCollection from musify.utils import to_collection diff --git a/tests/libraries/local/conftest.py b/tests/libraries/local/conftest.py index ed7a9909..78cb57ed 100644 --- a/tests/libraries/local/conftest.py +++ b/tests/libraries/local/conftest.py @@ -4,8 +4,8 @@ from musify.file.path_mapper import PathStemMapper from musify.libraries.local.track import LocalTrack, FLAC, MP3, M4A, WMA -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler from tests.libraries.local.utils import path_track_all, path_track_flac, path_track_mp3, path_track_m4a, path_track_wma from tests.utils import path_resources diff --git a/tests/libraries/local/library/test_musicbee.py b/tests/libraries/local/library/test_musicbee.py index 90175524..a86f0e0e 100644 --- a/tests/libraries/local/library/test_musicbee.py +++ b/tests/libraries/local/library/test_musicbee.py @@ -10,7 +10,7 @@ from musify.libraries.local.library import LocalLibrary, MusicBee from musify.libraries.local.library.musicbee import XMLLibraryParser, REQUIRED_MODULES from musify.libraries.local.track import LocalTrack -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.processors.filter import FilterIncludeExclude, FilterDefinedList from musify.utils import required_modules_installed from tests.libraries.local.library.testers import LocalLibraryTester diff --git a/tests/libraries/local/utils.py b/tests/libraries/local/utils.py index f5214deb..3ec48a70 100644 --- a/tests/libraries/local/utils.py +++ b/tests/libraries/local/utils.py @@ -3,7 +3,7 @@ from musify.libraries.local.playlist import PLAYLIST_CLASSES, XAutoPF from musify.libraries.local.playlist.xautopf import REQUIRED_MODULES as REQUIRED_XAUTOPF_MODULES from musify.libraries.local.track import TRACK_CLASSES -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler from musify.utils import required_modules_installed from tests.utils import path_resources diff --git a/tests/libraries/remote/core/processors/check.py b/tests/libraries/remote/core/processors/check.py index 62570603..c1fc9dc7 100644 --- a/tests/libraries/remote/core/processors/check.py +++ b/tests/libraries/remote/core/processors/check.py @@ -11,7 +11,7 @@ from musify.libraries.local.track import LocalTrack from musify.libraries.remote.core.enum import RemoteObjectType from musify.libraries.remote.core.object import RemotePlaylist -from musify.libraries.remote.core.processors.check import RemoteItemChecker +from musify.processors.check import RemoteItemChecker from tests.api.utils import path_token from tests.conftest import LogCapturer from tests.core.printer import PrettyPrinterTester diff --git a/tests/libraries/remote/core/processors/search.py b/tests/libraries/remote/core/processors/search.py index 2581753a..df206cbe 100644 --- a/tests/libraries/remote/core/processors/search.py +++ b/tests/libraries/remote/core/processors/search.py @@ -13,7 +13,7 @@ from musify.libraries.local.collection import LocalAlbum from musify.libraries.local.track import LocalTrack from musify.libraries.remote.core.enum import RemoteObjectType -from musify.libraries.remote.core.processors.search import RemoteItemSearcher, SearchConfig +from musify.processors.search import RemoteItemSearcher, SearchConfig from tests.core.printer import PrettyPrinterTester from tests.libraries.local.track.utils import random_track, random_tracks from tests.libraries.remote.core.utils import RemoteMock diff --git a/tests/libraries/remote/spotify/api/mock.py b/tests/libraries/remote/spotify/api/mock.py index 1d9091ad..394267f0 100644 --- a/tests/libraries/remote/spotify/api/mock.py +++ b/tests/libraries/remote/spotify/api/mock.py @@ -13,7 +13,7 @@ from musify.libraries.remote.core.enum import RemoteObjectType as ObjectType from musify.libraries.remote.spotify.api import SpotifyAPI -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler from tests.libraries.remote.core.utils import RemoteMock from tests.libraries.remote.spotify.utils import random_id from tests.utils import random_str, random_date_str, random_dt, random_genres diff --git a/tests/libraries/remote/spotify/conftest.py b/tests/libraries/remote/spotify/conftest.py index 58a10897..e160285a 100644 --- a/tests/libraries/remote/spotify/conftest.py +++ b/tests/libraries/remote/spotify/conftest.py @@ -3,7 +3,7 @@ import pytest from musify.libraries.remote.spotify.api import SpotifyAPI -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler from tests.libraries.remote.spotify.api.mock import SpotifyMock diff --git a/tests/libraries/remote/spotify/test_processors.py b/tests/libraries/remote/spotify/test_processors.py index 6cc5ed2f..d61232df 100644 --- a/tests/libraries/remote/spotify/test_processors.py +++ b/tests/libraries/remote/spotify/test_processors.py @@ -14,12 +14,12 @@ from musify.libraries.remote.core.base import RemoteResponse from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType from musify.libraries.remote.core.exception import RemoteError, RemoteIDTypeError, RemoteObjectTypeError -from musify.libraries.remote.core.processors.check import RemoteItemChecker -from musify.libraries.remote.core.processors.search import SearchConfig, RemoteItemSearcher +from musify.processors.check import RemoteItemChecker +from musify.processors.search import SearchConfig, RemoteItemSearcher from musify.libraries.remote.spotify.api import SpotifyAPI from musify.libraries.remote.spotify.factory import SpotifyObjectFactory from musify.libraries.remote.spotify.object import SpotifyTrack, SpotifyAlbum, SpotifyPlaylist -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler from musify.processors.match import CleanTagConfig, ItemMatcher from tests.libraries.local.track.utils import random_track from tests.libraries.remote.core.processors.check import RemoteItemCheckerTester diff --git a/tests/libraries/remote/spotify/utils.py b/tests/libraries/remote/spotify/utils.py index d179e5a6..6cd82aeb 100644 --- a/tests/libraries/remote/spotify/utils.py +++ b/tests/libraries/remote/spotify/utils.py @@ -3,9 +3,9 @@ from typing import Any from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType -from musify.libraries.remote.core.processors.wrangle import RemoteDataWrangler +from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.libraries.remote.spotify.base import SpotifyObject -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler from tests.libraries.remote.core.utils import ALL_ID_TYPES from tests.utils import random_str diff --git a/tests/test_report.py b/tests/test_report.py index 25431490..b3b1e582 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -10,7 +10,7 @@ from musify.libraries.remote.spotify.api import SpotifyAPI from musify.libraries.remote.spotify.library import SpotifyLibrary from musify.libraries.remote.spotify.object import SpotifyPlaylist -from musify.libraries.remote.spotify.processors import SpotifyDataWrangler +from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler from musify.report import report_playlist_differences, report_missing_tags from tests.libraries.local.track.utils import random_track from tests.libraries.remote.spotify.api.mock import SpotifyMock From 9e32b6c5b3538984a72b2ebd6fc057c9fd02c6fd Mon Sep 17 00:00:00 2001 From: geo-martino Date: Tue, 11 Jun 2024 15:07:20 -0400 Subject: [PATCH 2/4] finalise restructure --- docs/_howto/scripts/setup.py | 2 +- docs/release-history.rst | 3 + musify/api/authorise.py | 2 +- musify/api/cache/backend/base.py | 4 +- musify/api/cache/backend/sqlite.py | 2 +- musify/api/request.py | 2 +- musify/{core => }/base.py | 14 +- musify/core/__init__.py | 3 - musify/core/enum.py | 292 ------------------ musify/core/result.py | 11 - musify/enum.py | 58 ++++ musify/field.py | 242 ++++++++++++++- musify/file/path_mapper.py | 2 +- musify/libraries/collection.py | 2 +- musify/libraries/core/collection.py | 5 +- musify/libraries/core/object.py | 2 +- musify/libraries/local/base.py | 2 +- musify/libraries/local/collection.py | 4 +- musify/libraries/local/library/library.py | 6 +- musify/libraries/local/library/musicbee.py | 2 +- musify/libraries/local/playlist/base.py | 2 +- musify/libraries/local/playlist/m3u.py | 2 +- musify/libraries/local/playlist/xautopf.py | 7 +- musify/libraries/local/track/__init__.py | 2 +- musify/libraries/local/track/_tags/base.py | 2 +- musify/libraries/local/track/_tags/writer.py | 4 +- musify/libraries/local/track/field.py | 2 +- musify/libraries/local/track/flac.py | 4 +- musify/libraries/local/track/m4a.py | 2 +- musify/libraries/local/track/mp3.py | 4 +- musify/libraries/local/track/track.py | 6 +- musify/libraries/local/track/wma.py | 2 +- musify/libraries/remote/core/api.py | 4 +- musify/libraries/remote/core/base.py | 2 +- musify/libraries/remote/core/enum.py | 2 +- musify/libraries/remote/core/library.py | 6 +- musify/libraries/remote/core/object.py | 3 +- musify/libraries/remote/core/response.py | 2 +- musify/log/__init__.py | 18 -- musify/log/filter.py | 102 ------ musify/log/handlers.py | 102 ------ musify/{log => }/logger.py | 15 +- musify/{core => }/printer.py | 0 musify/processors/base.py | 4 +- musify/processors/check.py | 11 +- musify/processors/compare.py | 4 +- musify/processors/download.py | 4 +- musify/processors/filter.py | 2 +- musify/processors/filter_matcher.py | 6 +- musify/processors/limit.py | 5 +- musify/processors/match.py | 8 +- musify/processors/search.py | 9 +- musify/processors/sort.py | 5 +- musify/processors/time.py | 2 +- musify/report.py | 8 +- musify/utils.py | 3 + tests/__resources/test_logging.yml | 23 +- tests/conftest.py | 15 +- tests/core/base.py | 4 +- tests/core/enum.py | 5 +- tests/core/printer.py | 2 +- tests/libraries/core/collection.py | 4 +- .../libraries/local/playlist/test_xautopf.py | 2 +- tests/libraries/local/track/test_track.py | 2 +- tests/libraries/remote/core/library.py | 2 +- .../remote/core/processors/search.py | 4 +- .../remote/spotify/test_processors.py | 2 +- tests/log/__init__.py | 0 tests/log/test_handlers.py | 107 ------- tests/processors/test_download.py | 2 +- tests/processors/test_filter.py | 2 +- tests/processors/test_match.py | 2 +- tests/{log => }/test_logger.py | 55 +--- tests/utils.py | 2 +- 74 files changed, 431 insertions(+), 830 deletions(-) rename musify/{core => }/base.py (85%) delete mode 100644 musify/core/__init__.py delete mode 100644 musify/core/enum.py delete mode 100644 musify/core/result.py create mode 100644 musify/enum.py delete mode 100644 musify/log/__init__.py delete mode 100644 musify/log/filter.py delete mode 100644 musify/log/handlers.py rename musify/{log => }/logger.py (93%) rename musify/{core => }/printer.py (100%) delete mode 100644 tests/log/__init__.py delete mode 100644 tests/log/test_handlers.py rename tests/{log => }/test_logger.py (75%) diff --git a/docs/_howto/scripts/setup.py b/docs/_howto/scripts/setup.py index 5b208e1e..81298648 100644 --- a/docs/_howto/scripts/setup.py +++ b/docs/_howto/scripts/setup.py @@ -1,6 +1,6 @@ import logging import sys -from musify.log import STAT +from musify.logger import STAT logging.basicConfig(format="%(message)s", level=STAT, stream=sys.stdout) diff --git a/docs/release-history.rst b/docs/release-history.rst index 7db8976d..30b84a10 100644 --- a/docs/release-history.rst +++ b/docs/release-history.rst @@ -104,6 +104,9 @@ Changed * Moved :py:class:`.RemoteDataWrangler` up a level to `musify.libraries.remote.core`. * Deleted `musify.libraries.remote.core.processors` package. * Renamed `musify.libraries.remote.spotify.processors` module to `musify.libraries.remote.spotify.wrangle`. +* Removed logger filters and handlers. Moved to CLI repo. +* Moved `musify.logger` module to `musify` base package. +* Restructured contents of `musify.core` package to modules in `musify` base package. Fixed ----- diff --git a/musify/api/authorise.py b/musify/api/authorise.py index 529f084a..53d8d6d2 100644 --- a/musify/api/authorise.py +++ b/musify/api/authorise.py @@ -16,7 +16,7 @@ from musify import PROGRAM_NAME from musify.api.exception import APIError -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger class APIAuthoriser: diff --git a/musify/api/cache/backend/base.py b/musify/api/cache/backend/base.py index 4e0ab3e6..8c1ddac6 100644 --- a/musify/api/cache/backend/base.py +++ b/musify/api/cache/backend/base.py @@ -5,12 +5,12 @@ from datetime import datetime, timedelta from typing import Any, Self -from dateutil.relativedelta import relativedelta from aiohttp import RequestInfo, ClientRequest, ClientResponse +from dateutil.relativedelta import relativedelta from yarl import URL from musify.api.exception import CacheError -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger from musify.types import UnitCollection from musify.utils import to_collection diff --git a/musify/api/cache/backend/sqlite.py b/musify/api/cache/backend/sqlite.py index c084f641..b7ff5562 100644 --- a/musify/api/cache/backend/sqlite.py +++ b/musify/api/cache/backend/sqlite.py @@ -8,8 +8,8 @@ from tempfile import gettempdir from typing import Any, Self -from dateutil.relativedelta import relativedelta from aiohttp import RequestInfo, ClientRequest, ClientResponse +from dateutil.relativedelta import relativedelta from yarl import URL from musify import PROGRAM_NAME diff --git a/musify/api/request.py b/musify/api/request.py index 62ffbf43..6a82537c 100644 --- a/musify/api/request.py +++ b/musify/api/request.py @@ -20,7 +20,7 @@ from musify.api.cache.backend import ResponseCache from musify.api.cache.session import CachedSession from musify.api.exception import APIError, RequestError -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger from musify.utils import clean_kwargs diff --git a/musify/core/base.py b/musify/base.py similarity index 85% rename from musify/core/base.py rename to musify/base.py index 7fd85a00..615cfc82 100644 --- a/musify/core/base.py +++ b/musify/base.py @@ -4,12 +4,22 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod +from dataclasses import dataclass from typing import Any -from musify.core.enum import TagField -from musify.core.printer import AttributePrinter +from musify.field import TagField +from musify.printer import AttributePrinter +@dataclass(frozen=True) +class Result(metaclass=ABCMeta): + """Stores the results of an operation""" + pass + + +########################################################################### +## Core item/object types +########################################################################### class MusifyObject(AttributePrinter, metaclass=ABCMeta): """Generic base class for any nameable and taggable object.""" diff --git a/musify/core/__init__.py b/musify/core/__init__.py deleted file mode 100644 index 6120d319..00000000 --- a/musify/core/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -All core abstract classes for the entire package. -""" diff --git a/musify/core/enum.py b/musify/core/enum.py deleted file mode 100644 index b0a03ea7..00000000 --- a/musify/core/enum.py +++ /dev/null @@ -1,292 +0,0 @@ -""" -The fundamental core enum classes for the entire package. -""" -from collections.abc import Sequence -from dataclasses import dataclass, field -from enum import IntEnum -from typing import Self - -from musify.exception import MusifyEnumError -from musify.types import UnitIterable -from musify.utils import unique_list - - -class MusifyEnum(IntEnum): - """Generic class for :py:class:`IntEnum` implementations for the entire package.""" - - @classmethod - def map(cls, enum: Self) -> list[Self]: - """ - Optional mapper to apply to the enum found during :py:meth:`all`, :py:meth:`from_name`, - and :py:meth:`from_value` calls - """ - return [enum] - - @classmethod - def all(cls) -> list[Self]: - """Get all enums for this enum.""" - return unique_list(e for enum in cls if enum.name != "ALL" for e in cls.map(enum)) - - @classmethod - def from_name(cls, *names: str, fail_on_many: bool = True) -> list[Self]: - """ - Returns all enums that match the given enum names - - :param fail_on_many: If more than one enum is found, raise an exception. - :raise EnumNotFoundError: If a corresponding enum cannot be found. - """ - names_upper = [name.strip().upper() for name in names] - enums = unique_list(e for enum in cls if enum.name in names_upper for e in cls.map(enum)) - - if len(enums) == 0: - raise MusifyEnumError(names) - elif len(enums) > 1 and fail_on_many: - raise MusifyEnumError(value=enums, message="Too many enums found") - - return enums - - @classmethod - def from_value(cls, *values: int, fail_on_many: bool = True) -> list[Self]: - """ - Returns all enums that match the given enum values - - :param fail_on_many: If more than one enum is found, raise an exception. - :raise EnumNotFoundError: If a corresponding enum cannot be found. - """ - enums = unique_list(e for enum in cls if enum.value in values for e in cls.map(enum)) - if len(enums) == 0: - raise MusifyEnumError(values) - elif len(enums) > 1 and fail_on_many: - raise MusifyEnumError(value=enums, message="Too many enums found") - return enums - - -class Field(MusifyEnum): - """Base class for field names of an item.""" - - @classmethod - def from_name(cls, *names: str) -> list[Self]: - """ - Returns all enums that match the given enum names - - :raise EnumNotFoundError: If a corresponding enum cannot be found. - """ - return super().from_name(*names, fail_on_many=False) - - @classmethod - def from_value(cls, *values: int) -> list[Self]: - """ - Returns all enums that match the given enum values - - :raise EnumNotFoundError: If a corresponding enum cannot be found. - """ - return super().from_value(*values, fail_on_many=False) - - -class Fields(Field): - """ - All possible Field enums in this program. - - This is used to ensure all Field enum implementations have the same values for their enum names. - """ - # all numbers relate to the number of the field value as mapped by MusicBee - # changing these numbers will break all MusicBee functionality unless it is noted - # that a mapping with a MusicBee field is not present for that enum - - ALL = 0 - NAME = 1000 - - # tags/core properties - TITLE = 65 - ARTIST = 32 - ARTISTS = 108 - ALBUM = 30 # MusicBee album ignoring articles like 'the' and 'a' etc. - ALBUM_ARTIST = 31 - TRACK_NUMBER = 86 - TRACK_TOTAL = 87 - GENRES = 59 - DATE = 900 # no MusicBee mapping - YEAR = 35 - MONTH = 901 # no MusicBee mapping - DAY = 902 # no MusicBee mapping - BPM = 85 - KEY = 903 # unknown MusicBee mapping - DISC_NUMBER = 52 - DISC_TOTAL = 54 - COMPILATION = 904 # unknown MusicBee mapping - COMMENTS = 44 - IMAGES = 905 # no MusicBee mapping - LENGTH = 16 - RATING = 75 - COMPOSER = 43 - CONDUCTOR = 45 - PUBLISHER = 73 - - # file properties - PATH = 106 - FOLDER = 179 - FILENAME = 3 - EXT = 100 - SIZE = 7 - TYPE = 4 - CHANNELS = 8 - BIT_RATE = 10 - BIT_DEPTH = 183 - SAMPLE_RATE = 9 - - # date properties - DATE_CREATED = 921 # no MusicBee mapping - DATE_MODIFIED = 11 - DATE_ADDED = 12 - LAST_PLAYED = 13 - - # miscellaneous properties - PLAY_COUNT = 14 - DESCRIPTION = 931 # no MusicBee mapping - - # remote properties - URI = 941 # no MusicBee mapping - USER_ID = 942 # no MusicBee mapping - USER_NAME = 943 # no MusicBee mapping - OWNER_ID = 944 # no MusicBee mapping - OWNER_NAME = 945 # no MusicBee mapping - FOLLOWERS = 946 # no MusicBee mapping - - -@dataclass(frozen=True) -class TagMap: - """Map of human-friendly tag name to ID3 tag ids for a file type""" - # helpful for determining tags map: https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping - - title: Sequence[str] = field(default=()) - artist: Sequence[str] = field(default=()) - album: Sequence[str] = field(default=()) - album_artist: Sequence[str] = field(default=()) - track_number: Sequence[str] = field(default=()) - track_total: Sequence[str] = field(default=()) - genres: Sequence[str] = field(default=()) - date: Sequence[str] = field(default=()) - year: Sequence[str] = field(default=()) - month: Sequence[str] = field(default=()) - day: Sequence[str] = field(default=()) - bpm: Sequence[str] = field(default=()) - key: Sequence[str] = field(default=()) - disc_number: Sequence[str] = field(default=()) - disc_total: Sequence[str] = field(default=()) - compilation: Sequence[str] = field(default=()) - comments: Sequence[str] = field(default=()) - images: Sequence[str] = field(default=()) - - def __getitem__(self, key: str) -> Sequence[str]: - """Safely get the value of a given attribute key, returning an empty string if the key is not found""" - return getattr(self, key, []) - - -class TagField(Field): - """Applies extra functionality to :py:class:`Field` for objects which contain modifiable tags""" - - __tags__: frozenset[str] = frozenset(list(TagMap.__annotations__.keys()) + ["uri"]) - - @classmethod - def all(cls, only_tags: bool = False) -> list[Self]: - """ - Get all enums for this enum. - When ``only_tags`` is True, returns only those enums that represent a tag for this TagField type. - """ - enums = super().all() - if not only_tags: - return enums - - return list(sorted(cls.from_name(*cls.to_tags(enums)), key=lambda x: enums.index(x))) - - def to_tag(self) -> set[str]: - """ - Returns all human-friendly tag names for the current enum value. - - Applies mapper to enums before returning as per :py:meth:`map`. - This will only return tag names if they are found in :py:class:`TagMap`. - """ - if self == Fields.ALL: - return {tag.name.lower() for tag in super().all() if tag.name.lower() in self.__tags__} - return {tag.name.lower() for tag in self.map(self) if tag.name.lower() in self.__tags__} - - @classmethod - def to_tags(cls, tags: UnitIterable[Self]) -> set[str]: - """ - Returns all human-friendly tag names for the given enum value. - - Applies mapper to enums before returning as per :py:meth:`map`. - This will only return tag names if they are found in :py:class:`TagMap`. - """ - if isinstance(tags, cls): - return tags.to_tag() - return {t for tag in tags for t in tag.to_tag()} - - -class TagFields(TagField): - """ - All possible TagField enums in this program. - - This is used to ensure all TagField enum implementations have the same values for their enum names. - """ - ALL = Fields.ALL.value - NAME = Fields.NAME.value - - # tags/core properties - TITLE = Fields.TITLE.value - ARTIST = Fields.ARTIST.value - ALBUM = Fields.ALBUM.value - ALBUM_ARTIST = Fields.ALBUM_ARTIST.value - TRACK_NUMBER = Fields.TRACK_NUMBER.value - TRACK_TOTAL = Fields.TRACK_TOTAL.value - GENRES = Fields.GENRES.value - DATE = Fields.DATE.value - YEAR = Fields.YEAR.value - MONTH = Fields.MONTH.value - DAY = Fields.DAY.value - BPM = Fields.BPM.value - KEY = Fields.KEY.value - DISC_NUMBER = Fields.DISC_NUMBER.value - DISC_TOTAL = Fields.DISC_TOTAL.value - COMPILATION = Fields.COMPILATION.value - COMMENTS = Fields.COMMENTS.value - IMAGES = Fields.IMAGES.value - LENGTH = Fields.LENGTH.value - RATING = Fields.RATING.value - COMPOSER = Fields.COMPOSER.value - CONDUCTOR = Fields.CONDUCTOR.value - PUBLISHER = Fields.PUBLISHER.value - - # file properties - PATH = Fields.PATH.value - FOLDER = Fields.FOLDER.value - FILENAME = Fields.FILENAME.value - EXT = Fields.EXT.value - SIZE = Fields.SIZE.value - TYPE = Fields.TYPE.value - CHANNELS = Fields.CHANNELS.value - BIT_RATE = Fields.BIT_RATE.value - BIT_DEPTH = Fields.BIT_DEPTH.value - SAMPLE_RATE = Fields.SAMPLE_RATE.value - - # date properties - DATE_CREATED = Fields.DATE_CREATED.value - DATE_MODIFIED = Fields.DATE_MODIFIED.value - DATE_ADDED = Fields.DATE_ADDED.value - LAST_PLAYED = Fields.LAST_PLAYED.value - - # miscellaneous properties - PLAY_COUNT = Fields.PLAY_COUNT.value - DESCRIPTION = Fields.DESCRIPTION.value - - # remote properties - URI = Fields.URI.value - USER_ID = Fields.USER_ID.value - USER_NAME = Fields.USER_NAME.value - OWNER_ID = Fields.OWNER_ID.value - OWNER_NAME = Fields.OWNER_NAME.value - FOLLOWERS = Fields.FOLLOWERS.value - - -ALL_FIELDS = frozenset(Fields.all()) -ALL_TAG_FIELDS = frozenset(TagFields.all()) diff --git a/musify/core/result.py b/musify/core/result.py deleted file mode 100644 index 54645c72..00000000 --- a/musify/core/result.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -The fundamental core result classes for the entire package. -""" -from abc import ABCMeta -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Result(metaclass=ABCMeta): - """Stores the results of an operation""" - pass diff --git a/musify/enum.py b/musify/enum.py new file mode 100644 index 00000000..e28abf38 --- /dev/null +++ b/musify/enum.py @@ -0,0 +1,58 @@ +""" +The fundamental core enum classes for the entire package. +""" +from enum import IntEnum +from typing import Self + +from musify.exception import MusifyEnumError +from musify.utils import unique_list + + +class MusifyEnum(IntEnum): + """Generic class for :py:class:`IntEnum` implementations for the entire package.""" + + @classmethod + def map(cls, enum: Self) -> list[Self]: + """ + Optional mapper to apply to the enum found during :py:meth:`all`, :py:meth:`from_name`, + and :py:meth:`from_value` calls + """ + return [enum] + + @classmethod + def all(cls) -> list[Self]: + """Get all enums for this enum.""" + return unique_list(e for enum in cls if enum.name != "ALL" for e in cls.map(enum)) + + @classmethod + def from_name(cls, *names: str, fail_on_many: bool = True) -> list[Self]: + """ + Returns all enums that match the given enum names + + :param fail_on_many: If more than one enum is found, raise an exception. + :raise EnumNotFoundError: If a corresponding enum cannot be found. + """ + names_upper = [name.strip().upper() for name in names] + enums = unique_list(e for enum in cls if enum.name in names_upper for e in cls.map(enum)) + + if len(enums) == 0: + raise MusifyEnumError(names) + elif len(enums) > 1 and fail_on_many: + raise MusifyEnumError(value=enums, message="Too many enums found") + + return enums + + @classmethod + def from_value(cls, *values: int, fail_on_many: bool = True) -> list[Self]: + """ + Returns all enums that match the given enum values + + :param fail_on_many: If more than one enum is found, raise an exception. + :raise EnumNotFoundError: If a corresponding enum cannot be found. + """ + enums = unique_list(e for enum in cls if enum.value in values for e in cls.map(enum)) + if len(enums) == 0: + raise MusifyEnumError(values) + elif len(enums) > 1 and fail_on_many: + raise MusifyEnumError(value=enums, message="Too many enums found") + return enums diff --git a/musify/field.py b/musify/field.py index f0b43657..e8c2ccf6 100644 --- a/musify/field.py +++ b/musify/field.py @@ -2,11 +2,251 @@ All core :py:class:`Field` implementations relating to core :py:class:`MusifyItem` and :py:class`MusifyCollection` implementations. """ +from collections.abc import Sequence +from dataclasses import field, dataclass from typing import Self -from musify.core.enum import Field, Fields, TagField, TagFields +from musify.enum import MusifyEnum +from musify.types import UnitIterable +class Field(MusifyEnum): + """Base class for field names of an item.""" + + @classmethod + def from_name(cls, *names: str) -> list[Self]: + """ + Returns all enums that match the given enum names + + :raise EnumNotFoundError: If a corresponding enum cannot be found. + """ + return super().from_name(*names, fail_on_many=False) + + @classmethod + def from_value(cls, *values: int) -> list[Self]: + """ + Returns all enums that match the given enum values + + :raise EnumNotFoundError: If a corresponding enum cannot be found. + """ + return super().from_value(*values, fail_on_many=False) + + +class Fields(Field): + """ + All possible Field enums in this program. + + This is used to ensure all Field enum implementations have the same values for their enum names. + """ + # all numbers relate to the number of the field value as mapped by MusicBee + # changing these numbers will break all MusicBee functionality unless it is noted + # that a mapping with a MusicBee field is not present for that enum + + ALL = 0 + NAME = 1000 + + # tags/core properties + TITLE = 65 + ARTIST = 32 + ARTISTS = 108 + ALBUM = 30 # MusicBee album ignoring articles like 'the' and 'a' etc. + ALBUM_ARTIST = 31 + TRACK_NUMBER = 86 + TRACK_TOTAL = 87 + GENRES = 59 + DATE = 900 # no MusicBee mapping + YEAR = 35 + MONTH = 901 # no MusicBee mapping + DAY = 902 # no MusicBee mapping + BPM = 85 + KEY = 903 # unknown MusicBee mapping + DISC_NUMBER = 52 + DISC_TOTAL = 54 + COMPILATION = 904 # unknown MusicBee mapping + COMMENTS = 44 + IMAGES = 905 # no MusicBee mapping + LENGTH = 16 + RATING = 75 + COMPOSER = 43 + CONDUCTOR = 45 + PUBLISHER = 73 + + # file properties + PATH = 106 + FOLDER = 179 + FILENAME = 3 + EXT = 100 + SIZE = 7 + TYPE = 4 + CHANNELS = 8 + BIT_RATE = 10 + BIT_DEPTH = 183 + SAMPLE_RATE = 9 + + # date properties + DATE_CREATED = 921 # no MusicBee mapping + DATE_MODIFIED = 11 + DATE_ADDED = 12 + LAST_PLAYED = 13 + + # miscellaneous properties + PLAY_COUNT = 14 + DESCRIPTION = 931 # no MusicBee mapping + + # remote properties + URI = 941 # no MusicBee mapping + USER_ID = 942 # no MusicBee mapping + USER_NAME = 943 # no MusicBee mapping + OWNER_ID = 944 # no MusicBee mapping + OWNER_NAME = 945 # no MusicBee mapping + FOLLOWERS = 946 # no MusicBee mapping + + +########################################################################### +## Tags +########################################################################### +@dataclass(frozen=True) +class TagMap: + """Map of human-friendly tag name to ID3 tag ids for a file type""" + # helpful for determining tags map: https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping + + title: Sequence[str] = field(default=()) + artist: Sequence[str] = field(default=()) + album: Sequence[str] = field(default=()) + album_artist: Sequence[str] = field(default=()) + track_number: Sequence[str] = field(default=()) + track_total: Sequence[str] = field(default=()) + genres: Sequence[str] = field(default=()) + date: Sequence[str] = field(default=()) + year: Sequence[str] = field(default=()) + month: Sequence[str] = field(default=()) + day: Sequence[str] = field(default=()) + bpm: Sequence[str] = field(default=()) + key: Sequence[str] = field(default=()) + disc_number: Sequence[str] = field(default=()) + disc_total: Sequence[str] = field(default=()) + compilation: Sequence[str] = field(default=()) + comments: Sequence[str] = field(default=()) + images: Sequence[str] = field(default=()) + + def __getitem__(self, key: str) -> Sequence[str]: + """Safely get the value of a given attribute key, returning an empty string if the key is not found""" + return getattr(self, key, []) + + +class TagField(Field): + """Applies extra functionality to :py:class:`Field` for objects which contain modifiable tags""" + + __tags__: frozenset[str] = frozenset(list(TagMap.__annotations__.keys()) + ["uri"]) + + @classmethod + def all(cls, only_tags: bool = False) -> list[Self]: + """ + Get all enums for this enum. + When ``only_tags`` is True, returns only those enums that represent a tag for this TagField type. + """ + enums = super().all() + if not only_tags: + return enums + + return list(sorted(cls.from_name(*cls.to_tags(enums)), key=lambda x: enums.index(x))) + + def to_tag(self) -> set[str]: + """ + Returns all human-friendly tag names for the current enum value. + + Applies mapper to enums before returning as per :py:meth:`map`. + This will only return tag names if they are found in :py:class:`TagMap`. + """ + if self == Fields.ALL: + return {tag.name.lower() for tag in super().all() if tag.name.lower() in self.__tags__} + return {tag.name.lower() for tag in self.map(self) if tag.name.lower() in self.__tags__} + + @classmethod + def to_tags(cls, tags: UnitIterable[Self]) -> set[str]: + """ + Returns all human-friendly tag names for the given enum value. + + Applies mapper to enums before returning as per :py:meth:`map`. + This will only return tag names if they are found in :py:class:`TagMap`. + """ + if isinstance(tags, cls): + return tags.to_tag() + return {t for tag in tags for t in tag.to_tag()} + + +class TagFields(TagField): + """ + All possible TagField enums in this program. + + This is used to ensure all TagField enum implementations have the same values for their enum names. + """ + ALL = Fields.ALL.value + NAME = Fields.NAME.value + + # tags/core properties + TITLE = Fields.TITLE.value + ARTIST = Fields.ARTIST.value + ALBUM = Fields.ALBUM.value + ALBUM_ARTIST = Fields.ALBUM_ARTIST.value + TRACK_NUMBER = Fields.TRACK_NUMBER.value + TRACK_TOTAL = Fields.TRACK_TOTAL.value + GENRES = Fields.GENRES.value + DATE = Fields.DATE.value + YEAR = Fields.YEAR.value + MONTH = Fields.MONTH.value + DAY = Fields.DAY.value + BPM = Fields.BPM.value + KEY = Fields.KEY.value + DISC_NUMBER = Fields.DISC_NUMBER.value + DISC_TOTAL = Fields.DISC_TOTAL.value + COMPILATION = Fields.COMPILATION.value + COMMENTS = Fields.COMMENTS.value + IMAGES = Fields.IMAGES.value + LENGTH = Fields.LENGTH.value + RATING = Fields.RATING.value + COMPOSER = Fields.COMPOSER.value + CONDUCTOR = Fields.CONDUCTOR.value + PUBLISHER = Fields.PUBLISHER.value + + # file properties + PATH = Fields.PATH.value + FOLDER = Fields.FOLDER.value + FILENAME = Fields.FILENAME.value + EXT = Fields.EXT.value + SIZE = Fields.SIZE.value + TYPE = Fields.TYPE.value + CHANNELS = Fields.CHANNELS.value + BIT_RATE = Fields.BIT_RATE.value + BIT_DEPTH = Fields.BIT_DEPTH.value + SAMPLE_RATE = Fields.SAMPLE_RATE.value + + # date properties + DATE_CREATED = Fields.DATE_CREATED.value + DATE_MODIFIED = Fields.DATE_MODIFIED.value + DATE_ADDED = Fields.DATE_ADDED.value + LAST_PLAYED = Fields.LAST_PLAYED.value + + # miscellaneous properties + PLAY_COUNT = Fields.PLAY_COUNT.value + DESCRIPTION = Fields.DESCRIPTION.value + + # remote properties + URI = Fields.URI.value + USER_ID = Fields.USER_ID.value + USER_NAME = Fields.USER_NAME.value + OWNER_ID = Fields.OWNER_ID.value + OWNER_NAME = Fields.OWNER_NAME.value + FOLLOWERS = Fields.FOLLOWERS.value + + +ALL_FIELDS = frozenset(Fields.all()) +ALL_TAG_FIELDS = frozenset(TagFields.all()) + + +########################################################################### +## Object-specific fields +########################################################################### class TrackFieldMixin(TagField): """Applies extra functionality to the TagField enum for TagField types relating to :py:class:`Track` types""" diff --git a/musify/file/path_mapper.py b/musify/file/path_mapper.py index 3aecb004..5d647f23 100644 --- a/musify/file/path_mapper.py +++ b/musify/file/path_mapper.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Any -from musify.core.printer import PrettyPrinter from musify.file.base import File +from musify.printer import PrettyPrinter type PathInputType = str | Path | File | None diff --git a/musify/libraries/collection.py b/musify/libraries/collection.py index eb0cb19e..fa872fc8 100644 --- a/musify/libraries/collection.py +++ b/musify/libraries/collection.py @@ -6,7 +6,7 @@ from collections.abc import Iterable, Collection from typing import Any -from musify.core.base import MusifyItem +from musify.base import MusifyItem from musify.libraries.core.collection import MusifyCollection from musify.utils import to_collection diff --git a/musify/libraries/core/collection.py b/musify/libraries/core/collection.py index 18c2eb14..0702939e 100644 --- a/musify/libraries/core/collection.py +++ b/musify/libraries/core/collection.py @@ -11,16 +11,15 @@ from yarl import URL -from musify.core.base import MusifyObject, MusifyItem, HasLength -from musify.core.enum import Field +from musify.base import MusifyObject, MusifyItem, HasLength from musify.exception import MusifyTypeError, MusifyKeyError, MusifyAttributeError +from musify.field import Field from musify.file.base import File from musify.libraries.remote.core import RemoteResponse from musify.libraries.remote.core.base import RemoteObject from musify.processors.sort import ShuffleMode, ItemSorter from musify.types import UnitSequence - type ItemGetterTypes = str | URL | MusifyItem | Path | File | RemoteResponse diff --git a/musify/libraries/core/object.py b/musify/libraries/core/object.py index c97f023a..c0b9cdc0 100644 --- a/musify/libraries/core/object.py +++ b/musify/libraries/core/object.py @@ -12,7 +12,7 @@ from yarl import URL -from musify.core.base import MusifyItem, HasLength +from musify.base import MusifyItem, HasLength from musify.exception import MusifyTypeError from musify.libraries.core.collection import MusifyCollection from musify.libraries.remote.core.enum import RemoteObjectType diff --git a/musify/libraries/local/base.py b/musify/libraries/local/base.py index 2a0e6043..a079f47b 100644 --- a/musify/libraries/local/base.py +++ b/musify/libraries/local/base.py @@ -3,7 +3,7 @@ """ from abc import ABCMeta -from musify.core.base import MusifyItemSettable +from musify.base import MusifyItemSettable from musify.file.base import File diff --git a/musify/libraries/local/collection.py b/musify/libraries/local/collection.py index 2fcb7916..fba795d5 100644 --- a/musify/libraries/local/collection.py +++ b/musify/libraries/local/collection.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import Any, Self -from musify.core.enum import Fields, TagField, TagFields +from musify.field import Fields, TagField, TagFields from musify.file.exception import UnexpectedPathError from musify.libraries.core.collection import MusifyCollection from musify.libraries.core.object import Track, Library, Folder, Album, Artist, Genre @@ -21,7 +21,7 @@ from musify.libraries.local.track import LocalTrack, SyncResultTrack, load_track, TRACK_FILETYPES from musify.libraries.local.track.field import LocalTrackField from musify.libraries.remote.core.wrangle import RemoteDataWrangler -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger from musify.types import UnitCollection, UnitIterable from musify.utils import get_most_common_values, to_collection, align_string, get_max_width diff --git a/musify/libraries/local/library/library.py b/musify/libraries/local/library/library.py index b3e55830..54d07d15 100644 --- a/musify/libraries/local/library/library.py +++ b/musify/libraries/local/library/library.py @@ -1,15 +1,15 @@ """ The core, basic library implementation which is just a simple set of folders. """ -import itertools import functools +import itertools import os from collections.abc import Collection, Mapping, Iterable from concurrent.futures import ThreadPoolExecutor from pathlib import Path from typing import Any -from musify.core.result import Result +from musify.base import Result from musify.exception import MusifyError from musify.file.path_mapper import PathMapper, PathStemMapper from musify.libraries.core.object import Library @@ -18,7 +18,7 @@ from musify.libraries.local.track import TRACK_CLASSES, LocalTrack, load_track from musify.libraries.local.track.field import LocalTrackField from musify.libraries.remote.core.wrangle import RemoteDataWrangler -from musify.log import STAT +from musify.logger import STAT from musify.processors.base import Filter from musify.processors.filter import FilterDefinedList from musify.processors.sort import ItemSorter diff --git a/musify/libraries/local/library/musicbee.py b/musify/libraries/local/library/musicbee.py index 4da5d63a..f5ec8322 100644 --- a/musify/libraries/local/library/musicbee.py +++ b/musify/libraries/local/library/musicbee.py @@ -3,8 +3,8 @@ Reads library/settings files from MusicBee to load and enrich playlist/track etc. data. """ import hashlib -import re import os +import re from collections.abc import Iterable, Mapping, Sequence, Collection, Iterator from datetime import datetime from pathlib import Path diff --git a/musify/libraries/local/playlist/base.py b/musify/libraries/local/playlist/base.py index f25426a8..8d8b1556 100644 --- a/musify/libraries/local/playlist/base.py +++ b/musify/libraries/local/playlist/base.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Any, Self -from musify.core.result import Result +from musify.base import Result from musify.file.base import File from musify.file.path_mapper import PathMapper from musify.libraries.core.object import Playlist diff --git a/musify/libraries/local/playlist/m3u.py b/musify/libraries/local/playlist/m3u.py index 18329521..10996b7e 100644 --- a/musify/libraries/local/playlist/m3u.py +++ b/musify/libraries/local/playlist/m3u.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Self -from musify.core.result import Result +from musify.base import Result from musify.file.base import File from musify.file.path_mapper import PathMapper from musify.libraries.local.playlist.base import LocalPlaylist diff --git a/musify/libraries/local/playlist/xautopf.py b/musify/libraries/local/playlist/xautopf.py index 62da0597..0850dc88 100644 --- a/musify/libraries/local/playlist/xautopf.py +++ b/musify/libraries/local/playlist/xautopf.py @@ -7,16 +7,15 @@ from pathlib import Path from typing import Any, Self -from musify.core.base import MusifyItem -from musify.core.enum import Fields, Field, TagFields -from musify.core.printer import PrettyPrinter -from musify.core.result import Result +from musify.base import MusifyItem, Result from musify.exception import FieldError, MusifyImportError +from musify.field import Fields, Field, TagFields from musify.file.base import File from musify.file.path_mapper import PathMapper from musify.libraries.local.playlist.base import LocalPlaylist from musify.libraries.local.track import LocalTrack from musify.libraries.remote.core.wrangle import RemoteDataWrangler +from musify.printer import PrettyPrinter from musify.processors.compare import Comparer from musify.processors.exception import SorterProcessorError from musify.processors.filter import FilterDefinedList, FilterComparers diff --git a/musify/libraries/local/track/__init__.py b/musify/libraries/local/track/__init__.py index 15599566..1e06452f 100644 --- a/musify/libraries/local/track/__init__.py +++ b/musify/libraries/local/track/__init__.py @@ -3,10 +3,10 @@ Specific audio file types should implement :py:class:`LocalTrack`. """ +from ._tags.writer import SyncResultTrack from .flac import FLAC from .m4a import M4A from .mp3 import MP3 -from ._tags.writer import SyncResultTrack from .track import LocalTrack from .utils import TRACK_CLASSES, TRACK_FILETYPES, load_track from .wma import WMA diff --git a/musify/libraries/local/track/_tags/base.py b/musify/libraries/local/track/_tags/base.py index 2e402119..981da767 100644 --- a/musify/libraries/local/track/_tags/base.py +++ b/musify/libraries/local/track/_tags/base.py @@ -5,7 +5,7 @@ import mutagen -from musify.core.enum import TagMap +from musify.field import TagMap from musify.libraries.local.track.field import LocalTrackField from musify.libraries.remote.core.wrangle import RemoteDataWrangler diff --git a/musify/libraries/local/track/_tags/writer.py b/musify/libraries/local/track/_tags/writer.py index 31b7e46d..8ade32d2 100644 --- a/musify/libraries/local/track/_tags/writer.py +++ b/musify/libraries/local/track/_tags/writer.py @@ -8,10 +8,10 @@ import mutagen -from musify.core.result import Result +from musify.base import Result from musify.libraries.core.object import Track -from musify.libraries.local.track.field import LocalTrackField as Tags from musify.libraries.local.track._tags.base import TagProcessor +from musify.libraries.local.track.field import LocalTrackField as Tags from musify.types import UnitIterable from musify.utils import to_collection diff --git a/musify/libraries/local/track/field.py b/musify/libraries/local/track/field.py index eb819ba8..a808259e 100644 --- a/musify/libraries/local/track/field.py +++ b/musify/libraries/local/track/field.py @@ -1,7 +1,7 @@ """ The core Field enum for a :py:class:`LocalTrack` representing all possible tags/metadata/properties. """ -from musify.core.enum import TagFields, Fields +from musify.field import TagFields, Fields from musify.field import TrackFieldMixin diff --git a/musify/libraries/local/track/flac.py b/musify/libraries/local/track/flac.py index 1491c868..7a63eb4a 100644 --- a/musify/libraries/local/track/flac.py +++ b/musify/libraries/local/track/flac.py @@ -8,11 +8,11 @@ import mutagen.flac import mutagen.id3 -from musify.core.enum import TagMap +from musify.field import TagMap from musify.file.image import open_image, get_image_bytes -from musify.libraries.local.track.field import LocalTrackField # noinspection PyProtectedMember from musify.libraries.local.track._tags import TagReader, TagWriter +from musify.libraries.local.track.field import LocalTrackField from musify.libraries.local.track.track import LocalTrack try: diff --git a/musify/libraries/local/track/m4a.py b/musify/libraries/local/track/m4a.py index 13a96284..8c76ee31 100644 --- a/musify/libraries/local/track/m4a.py +++ b/musify/libraries/local/track/m4a.py @@ -8,7 +8,7 @@ import mutagen import mutagen.mp4 -from musify.core.enum import TagMap +from musify.field import TagMap from musify.file.image import open_image, get_image_bytes # noinspection PyProtectedMember from musify.libraries.local.track._tags import TagReader, TagWriter diff --git a/musify/libraries/local/track/mp3.py b/musify/libraries/local/track/mp3.py index 0c61d7f1..3602d189 100644 --- a/musify/libraries/local/track/mp3.py +++ b/musify/libraries/local/track/mp3.py @@ -9,12 +9,12 @@ import mutagen.id3 import mutagen.mp3 -from musify.core.enum import TagMap +from musify.field import TagMap from musify.file.image import open_image, get_image_bytes from musify.libraries.local.exception import TagError -from musify.libraries.local.track.field import LocalTrackField # noinspection PyProtectedMember from musify.libraries.local.track._tags import TagReader, TagWriter +from musify.libraries.local.track.field import LocalTrackField from musify.libraries.local.track.track import LocalTrack try: diff --git a/musify/libraries/local/track/track.py b/musify/libraries/local/track/track.py index 09b3b302..0a6abe4b 100644 --- a/musify/libraries/local/track/track.py +++ b/musify/libraries/local/track/track.py @@ -12,16 +12,16 @@ import mutagen from yarl import URL -from musify.core.base import MusifyItem -from musify.core.enum import TagMap +from musify.base import MusifyItem from musify.exception import MusifyKeyError, MusifyAttributeError, MusifyTypeError, MusifyValueError +from musify.field import TagMap from musify.field import TrackField from musify.file.exception import FileDoesNotExistError, UnexpectedPathError from musify.libraries.core.object import Track from musify.libraries.local.base import LocalItem -from musify.libraries.local.track.field import LocalTrackField as Tags # noinspection PyProtectedMember from musify.libraries.local.track._tags import TagReader, TagWriter, SyncResultTrack +from musify.libraries.local.track.field import LocalTrackField as Tags from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.types import UnitIterable from musify.utils import to_collection diff --git a/musify/libraries/local/track/wma.py b/musify/libraries/local/track/wma.py index 9b2e16a9..9c8fac5e 100644 --- a/musify/libraries/local/track/wma.py +++ b/musify/libraries/local/track/wma.py @@ -10,7 +10,7 @@ import mutagen.asf import mutagen.id3 -from musify.core.enum import TagMap +from musify.field import TagMap from musify.file.image import open_image, get_image_bytes # noinspection PyProtectedMember from musify.libraries.local.track._tags import TagReader, TagWriter diff --git a/musify/libraries/remote/core/api.py b/musify/libraries/remote/core/api.py index de80b08d..645ce4e0 100644 --- a/musify/libraries/remote/core/api.py +++ b/musify/libraries/remote/core/api.py @@ -16,9 +16,9 @@ from musify.api.request import RequestHandler from musify.libraries.remote.core import RemoteResponse from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType -from musify.libraries.remote.core.wrangle import RemoteDataWrangler from musify.libraries.remote.core.types import APIInputValueSingle, APIInputValueMulti -from musify.log.logger import MusifyLogger +from musify.libraries.remote.core.wrangle import RemoteDataWrangler +from musify.logger import MusifyLogger from musify.types import UnitSequence, JSON, UnitList from musify.utils import align_string, to_collection diff --git a/musify/libraries/remote/core/base.py b/musify/libraries/remote/core/base.py index a164d0f3..ae09760f 100644 --- a/musify/libraries/remote/core/base.py +++ b/musify/libraries/remote/core/base.py @@ -9,7 +9,7 @@ from yarl import URL from musify.api.exception import APIError -from musify.core.base import MusifyItem +from musify.base import MusifyItem from musify.libraries.remote.core import RemoteResponse from musify.libraries.remote.core.api import RemoteAPI from musify.libraries.remote.core.types import APIInputValueSingle diff --git a/musify/libraries/remote/core/enum.py b/musify/libraries/remote/core/enum.py index e441e1d3..e0a80482 100644 --- a/musify/libraries/remote/core/enum.py +++ b/musify/libraries/remote/core/enum.py @@ -3,7 +3,7 @@ Represents ID and item types. """ -from musify.core.enum import MusifyEnum +from musify.enum import MusifyEnum class RemoteIDType(MusifyEnum): diff --git a/musify/libraries/remote/core/library.py b/musify/libraries/remote/core/library.py index 616efe93..a7b8910b 100644 --- a/musify/libraries/remote/core/library.py +++ b/musify/libraries/remote/core/library.py @@ -6,15 +6,15 @@ from collections.abc import Collection, Mapping, Iterable from typing import Any, Literal, Self -from musify.core.base import MusifyItem +from musify.base import MusifyItem from musify.libraries.core.object import Track, Library, Playlist from musify.libraries.remote.core.api import RemoteAPI from musify.libraries.remote.core.enum import RemoteObjectType from musify.libraries.remote.core.factory import RemoteObjectFactory from musify.libraries.remote.core.object import RemoteCollection, SyncResultRemotePlaylist from musify.libraries.remote.core.object import RemoteTrack, RemotePlaylist, RemoteArtist, RemoteAlbum -from musify.log import STAT -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger +from musify.logger import STAT from musify.processors.base import Filter from musify.processors.filter import FilterDefinedList from musify.utils import align_string, get_max_width, to_collection diff --git a/musify/libraries/remote/core/object.py b/musify/libraries/remote/core/object.py index 762cbf32..e83e0094 100644 --- a/musify/libraries/remote/core/object.py +++ b/musify/libraries/remote/core/object.py @@ -12,8 +12,7 @@ from typing import Self, Literal from musify.api.exception import APIError -from musify.core.base import MusifyItem -from musify.core.result import Result +from musify.base import MusifyItem, Result from musify.libraries.core.collection import MusifyCollection from musify.libraries.core.object import Track, Album, Playlist, Artist from musify.libraries.remote.core.api import RemoteAPI diff --git a/musify/libraries/remote/core/response.py b/musify/libraries/remote/core/response.py index 8817df47..c52a094d 100644 --- a/musify/libraries/remote/core/response.py +++ b/musify/libraries/remote/core/response.py @@ -5,7 +5,7 @@ from abc import ABCMeta, abstractmethod from typing import Any -from musify.core.base import MusifyObject +from musify.base import MusifyObject from musify.libraries.remote.core.enum import RemoteObjectType diff --git a/musify/log/__init__.py b/musify/log/__init__.py deleted file mode 100644 index 1ad9b4bc..00000000 --- a/musify/log/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -All classes and functions pertaining to logging operations throughout the package. -""" -import logging - -LOGGING_DT_FORMAT = "%Y-%m-%d_%H.%M.%S" - -INFO_EXTRA = logging.INFO - 1 -logging.addLevelName(INFO_EXTRA, "INFO_EXTRA") -logging.INFO_EXTRA = INFO_EXTRA - -REPORT = logging.INFO - 3 -logging.addLevelName(REPORT, "REPORT") -logging.REPORT = REPORT - -STAT = logging.DEBUG + 3 -logging.addLevelName(STAT, "STAT") -logging.STAT = STAT diff --git a/musify/log/filter.py b/musify/log/filter.py deleted file mode 100644 index 387f3a2f..00000000 --- a/musify/log/filter.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -All logging filters specific to this package. -""" -import inspect -import logging.handlers -import re -from pathlib import Path - -from musify import PROGRAM_NAME - - -def format_full_func_name(record: logging.LogRecord, width: int = 40) -> None: - """ - Set fully qualified path name to function including class name to the given record. - Optionally, provide a max ``width`` to attempt to truncate the path name to - by taking only the first letter of each part of the path until the length is equal to ``width``. - """ - last_call = inspect.stack()[8] - - if record.pathname == __file__: - # custom logging method has been called, reformat call info to actual call method - record.pathname = last_call.filename - record.lineno = last_call.lineno - record.funcName = last_call.function - record.filename = Path(record.pathname).name - record.module = record.name.split(".")[-1] - - f_locals = last_call.frame.f_locals - if "self" not in f_locals: - path_split = record.name.split(".") - if record.funcName != "": - path_split.append(record.funcName) - else: - # is a valid and initialised object, extract the class name and determine path to call function from stack - cls = f_locals["self"].__class__ - path = Path(inspect.getfile(cls)).with_suffix("").joinpath(Path(cls.__name__, record.funcName.split(".")[-1])) - - folder = "" - path_split = [] - while not folder.casefold().startswith(PROGRAM_NAME.casefold()): # get relative path to sources root - folder = path.name - path = path.parent - path_split.append(folder) - path_split.append(PROGRAM_NAME.lower()) - - # produce fully qualified path - path_split = path_split[:-1][::-1] - - # truncate long paths by taking first letters of each part until short enough - path = ".".join(path_split) - for i, part in enumerate(path_split): - if len(path) <= width: - break - if not part: - continue - - # take all upper case characters if they exist in part, else, if all lower case, take first letter - path_split[i] = re.sub("[a-z_]+", "", part) if re.match("[A-Z]", part) else part[0] - path = ".".join(path_split) - - record.funcName = path - - -class LogConsoleFilter(logging.Filter): - """ - Filter for logging to the console. - - :param module_width: The maximum width a module string can be in the log record. - Truncates module string if longer that this length. - """ - - __slots__ = ("module_width",) - - def __init__(self, name: str = "", module_width: int = 40): - super().__init__(name) - self.module_width = module_width - - # noinspection PyMissingOrEmptyDocstring - def filter(self, record: logging.LogRecord) -> logging.LogRecord | None: - format_full_func_name(record, width=self.module_width) - return record - - -class LogFileFilter(logging.Filter): - """ - Filter for logging to a file. - - :param module_width: The maximum width a module string can be in the log record. - Truncates module string if longer that this length. - """ - - __slots__ = ("module_width",) - - def __init__(self, name: str = "", module_width: int = 40): - super().__init__(name) - self.module_width = module_width - - # noinspection PyMissingOrEmptyDocstring - def filter(self, record: logging.LogRecord) -> logging.LogRecord: - record.msg = re.sub("\33.*?m", "", record.msg) - format_full_func_name(record, width=self.module_width) - return record diff --git a/musify/log/handlers.py b/musify/log/handlers.py deleted file mode 100644 index ef9535d8..00000000 --- a/musify/log/handlers.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -All logging handlers specific to this package. -""" -import logging.handlers -import os -import shutil -from datetime import datetime -from pathlib import Path - -from musify.log import LOGGING_DT_FORMAT -from musify.processors.time import TimeMapper - - -class CurrentTimeRotatingFileHandler(logging.handlers.BaseRotatingHandler): - """ - Handles log file and directory rotation based on log file/folder name. - - :param filename: The full path to the log file. - Optionally, include a '{}' part in the path to format in the current datetime. - When None, defaults to '{}.log' - :param encoding: When not None, it is used to open the file with that encoding. - :param when: The timespan for 'interval' which is used together to calculate the timedelta. - Accepts same values as :py:class:`TimeMapper`. - :param interval: The multiplier for ``when``. - When combined with ``when``, gives the negative timedelta relative to now - which is the maximum datetime to keep logs for. - :param count: The maximum number of files to keep. - :param delay: When True, the file opening is deferred until the first call to emit(). - :param errors: Used to determine how encoding errors are handled. - """ - - __slots__ = ("dt", "filename", "delta", "count", "removed") - - def __init__( - self, - filename: str | Path | None = None, - encoding: str | None = None, - when: str | None = None, - interval: int | None = None, - count: int | None = None, - delay: bool = False, - errors: str | None = None - ): - self.dt = datetime.now() - - dt_str = self.dt.strftime(LOGGING_DT_FORMAT) - if not filename: - filename = "{}.log" - - self.filename = Path(str(filename).format(dt_str) if "{}" in str(filename) else filename) - os.makedirs(self.filename.parent, exist_ok=True) - - self.delta = TimeMapper(when.lower())(interval) if when and interval else None - self.count = count - - self.removed: list[datetime] = [] # datetime on the files that were removed - self.rotator(unformatted=str(filename), formatted=self.filename) - - super().__init__(filename=self.filename, mode="w", encoding=encoding, delay=delay, errors=errors) - - # noinspection PyPep8Naming - @staticmethod - def shouldRollover(*_, **__) -> bool: - """Always returns False. Rotation happens on __init__ and only needs to happen once.""" - return False - - def rotator(self, unformatted: str, formatted: str | Path): - """ - Rotates the files in the folder on the given ``unformatted`` path. - Removes files older than ``self.delta`` and the oldest files when number of files >= count - until number of files <= count. ``formatted`` path is excluded from processing. - """ - formatted = Path(formatted) - folder = formatted.parent - if not folder: - return - - # get current files present and prefix+suffix to remove when processing - paths = tuple(f for f in list(folder.glob("*")) if f != formatted) - prefix = unformatted.split("{")[0] if "{" in unformatted and "}" in unformatted else "" - suffix = unformatted.split("}")[1] if "{" in unformatted and "}" in unformatted else "" - - remaining = len(paths) - for path in sorted(paths): - too_many = self.count is not None and remaining >= self.count - - dt_part = str(path).removeprefix(prefix).removesuffix(suffix) - try: - dt_file = datetime.strptime(dt_part, LOGGING_DT_FORMAT) - too_old = self.delta is not None and dt_file < self.dt - self.delta - except ValueError: - dt_file = None - too_old = False - - is_empty = path.is_dir() and not list(path.glob("*")) - - if too_many or too_old or is_empty: - os.remove(path) if path.is_file() else shutil.rmtree(path) - remaining -= 1 - - if dt_file and dt_file not in self.removed: - self.removed.append(dt_file) diff --git a/musify/log/logger.py b/musify/logger.py similarity index 93% rename from musify/log/logger.py rename to musify/logger.py index 794f2322..408fcc23 100644 --- a/musify/log/logger.py +++ b/musify/logger.py @@ -11,8 +11,6 @@ from pathlib import Path from typing import Any -from musify.log import INFO_EXTRA, REPORT, STAT - try: from tqdm.auto import tqdm except ImportError: @@ -21,6 +19,19 @@ type ProgressBarType[T] = Iterable[T] | tqdm if tqdm is not None else Iterable[T] +INFO_EXTRA = logging.INFO - 1 +logging.addLevelName(INFO_EXTRA, "INFO_EXTRA") +logging.INFO_EXTRA = INFO_EXTRA + +REPORT = logging.INFO - 3 +logging.addLevelName(REPORT, "REPORT") +logging.REPORT = REPORT + +STAT = logging.DEBUG + 3 +logging.addLevelName(STAT, "STAT") +logging.STAT = STAT + + class MusifyLogger(logging.Logger): """The logger for all logging operations in Musify.""" diff --git a/musify/core/printer.py b/musify/printer.py similarity index 100% rename from musify/core/printer.py rename to musify/printer.py diff --git a/musify/processors/base.py b/musify/processors/base.py index 413ee4fa..b2fe48b9 100644 --- a/musify/processors/base.py +++ b/musify/processors/base.py @@ -7,8 +7,8 @@ from functools import partial, update_wrapper from typing import Any, Optional -from musify.core.printer import PrettyPrinter -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger +from musify.printer import PrettyPrinter from musify.processors.exception import ProcessorLookupError from musify.utils import get_user_input, get_max_width, align_string diff --git a/musify/processors/check.py b/musify/processors/check.py index 03c94c49..67fd1bc5 100644 --- a/musify/processors/check.py +++ b/musify/processors/check.py @@ -13,19 +13,18 @@ from typing import Any, Self from musify import PROGRAM_NAME -from musify.core.base import MusifyItemSettable -from musify.core.enum import Fields -from musify.core.result import Result +from musify.base import MusifyItemSettable, Result +from musify.field import Fields from musify.libraries.core.collection import MusifyCollection from musify.libraries.remote.core.api import RemoteAPI from musify.libraries.remote.core.enum import RemoteIDType, RemoteObjectType from musify.libraries.remote.core.factory import RemoteObjectFactory from musify.libraries.remote.core.object import RemotePlaylist -from musify.processors.search import RemoteItemSearcher -from musify.log import REPORT -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger +from musify.logger import REPORT from musify.processors.base import InputProcessor from musify.processors.match import ItemMatcher +from musify.processors.search import RemoteItemSearcher from musify.utils import get_max_width, align_string try: diff --git a/musify/processors/compare.py b/musify/processors/compare.py index 0c8ff43a..17a45873 100644 --- a/musify/processors/compare.py +++ b/musify/processors/compare.py @@ -8,8 +8,8 @@ from operator import mul from typing import Any -from musify.core.base import MusifyItem -from musify.core.enum import Field +from musify.base import MusifyItem +from musify.field import Field from musify.processors.base import DynamicProcessor, dynamicprocessormethod from musify.processors.exception import ComparerError from musify.processors.time import TimeMapper diff --git a/musify/processors/download.py b/musify/processors/download.py index 979b266a..ffd53d74 100644 --- a/musify/processors/download.py +++ b/musify/processors/download.py @@ -7,9 +7,9 @@ from typing import Any from webbrowser import open as webopen -from musify.core.base import MusifyItem -from musify.core.enum import Field, Fields +from musify.base import MusifyItem from musify.exception import MusifyEnumError +from musify.field import Field, Fields from musify.libraries.core.collection import MusifyCollection from musify.processors.base import InputProcessor from musify.types import UnitIterable diff --git a/musify/processors/filter.py b/musify/processors/filter.py index 91fd57c5..6334550c 100644 --- a/musify/processors/filter.py +++ b/musify/processors/filter.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Any, Self -from musify.core.base import MusifyObject +from musify.base import MusifyObject from musify.processors.base import Filter, FilterComposite from musify.processors.compare import Comparer from musify.types import UnitCollection diff --git a/musify/processors/filter_matcher.py b/musify/processors/filter_matcher.py index 0dfc6e74..fa1f1236 100644 --- a/musify/processors/filter_matcher.py +++ b/musify/processors/filter_matcher.py @@ -8,9 +8,9 @@ from dataclasses import field, dataclass from typing import Any -from musify.core.enum import TagField -from musify.core.result import Result -from musify.log.logger import MusifyLogger +from musify.base import Result +from musify.field import TagField +from musify.logger import MusifyLogger from musify.processors.base import Filter, FilterComposite from musify.processors.filter import FilterComparers, FilterDefinedList diff --git a/musify/processors/limit.py b/musify/processors/limit.py index 7a4fe22f..9945cf5f 100644 --- a/musify/processors/limit.py +++ b/musify/processors/limit.py @@ -6,8 +6,9 @@ from operator import mul from random import shuffle -from musify.core.base import MusifyItem, HasLength -from musify.core.enum import MusifyEnum, Fields +from musify.base import MusifyItem, HasLength +from musify.enum import MusifyEnum +from musify.field import Fields from musify.file.base import File from musify.libraries.core.object import Track from musify.processors.base import DynamicProcessor, dynamicprocessormethod diff --git a/musify/processors/match.py b/musify/processors/match.py index d30f4fb9..a4bdb7f1 100644 --- a/musify/processors/match.py +++ b/musify/processors/match.py @@ -9,11 +9,11 @@ from dataclasses import dataclass, field from typing import Any -from musify.core.base import MusifyObject -from musify.core.enum import TagField, TagFields as Tag, ALL_TAG_FIELDS -from musify.core.printer import PrettyPrinter +from musify.base import MusifyObject +from musify.field import TagField, TagFields as Tag, ALL_TAG_FIELDS from musify.libraries.core.collection import MusifyCollection -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger +from musify.printer import PrettyPrinter from musify.processors.base import Processor from musify.types import UnitIterable from musify.utils import limit_value, to_collection diff --git a/musify/processors/search.py b/musify/processors/search.py index 013900ff..06f69cfe 100644 --- a/musify/processors/search.py +++ b/musify/processors/search.py @@ -9,16 +9,15 @@ from dataclasses import dataclass, field from typing import Any, Self -from musify.core.base import MusifyObject, MusifyItemSettable -from musify.core.enum import TagField, TagFields as Tag -from musify.core.result import Result +from musify.base import MusifyObject, MusifyItemSettable, Result from musify.exception import MusifyAttributeError +from musify.field import TagField, TagFields as Tag from musify.libraries.core.collection import MusifyCollection from musify.libraries.remote.core.api import RemoteAPI from musify.libraries.remote.core.enum import RemoteObjectType from musify.libraries.remote.core.factory import RemoteObjectFactory -from musify.log import REPORT -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger +from musify.logger import REPORT from musify.processors.base import Processor from musify.processors.match import ItemMatcher from musify.types import UnitIterable diff --git a/musify/processors/sort.py b/musify/processors/sort.py index 24886d2a..efd06316 100644 --- a/musify/processors/sort.py +++ b/musify/processors/sort.py @@ -8,8 +8,9 @@ from random import shuffle from typing import Any -from musify.core.base import MusifyItem -from musify.core.enum import MusifyEnum, Field +from musify.base import MusifyItem +from musify.enum import MusifyEnum +from musify.field import Field from musify.processors.base import Processor from musify.processors.exception import SorterProcessorError from musify.types import UnitSequence, UnitIterable, Number diff --git a/musify/processors/time.py b/musify/processors/time.py index d2877bc7..ec872f22 100644 --- a/musify/processors/time.py +++ b/musify/processors/time.py @@ -6,7 +6,7 @@ from dateutil.relativedelta import relativedelta -from musify.core.printer import PrettyPrinter +from musify.printer import PrettyPrinter from musify.processors.base import DynamicProcessor, dynamicprocessormethod diff --git a/musify/report.py b/musify/report.py index b233b14e..5dcdcbe8 100644 --- a/musify/report.py +++ b/musify/report.py @@ -4,13 +4,13 @@ import logging from collections.abc import Iterable -from musify.core.base import MusifyItem -from musify.core.enum import TagField, Fields, ALL_FIELDS, TagFields +from musify.base import MusifyItem +from musify.field import TagField, Fields, ALL_FIELDS, TagFields from musify.libraries.core.collection import MusifyCollection from musify.libraries.core.object import Library, Playlist from musify.libraries.local.library import LocalLibrary -from musify.log import REPORT -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger +from musify.logger import REPORT from musify.types import UnitIterable from musify.utils import align_string, get_max_width, to_collection diff --git a/musify/utils.py b/musify/utils.py index 7613517f..6320ff4c 100644 --- a/musify/utils.py +++ b/musify/utils.py @@ -13,6 +13,9 @@ from musify.types import Number +########################################################################### +## Extended primitives +########################################################################### class SafeDict(dict): """Extends dict to ignore missing keys when using format_map operations""" diff --git a/tests/__resources/test_logging.yml b/tests/__resources/test_logging.yml index 293207c0..fcfaef39 100644 --- a/tests/__resources/test_logging.yml +++ b/tests/__resources/test_logging.yml @@ -14,20 +14,11 @@ formatters: <<: *formatter_base format: '\33[91m{asctime}.{msecs:0<3.0f}\33[0m | [\33[92m{levelname:>8.8}\33[0m] \33[1;96m{funcName:<40.40}\33[0m [\33[95m{lineno:>4}\33[0m] | {message}' -filters: - console: - (): "musify.log.filter.LogConsoleFilter" - module_width: 40 - file: - (): "musify.log.filter.LogFileFilter" - module_width: 40 - handlers: console_debug: &console class: logging.StreamHandler level: DEBUG formatter: extended_colour - filters: ["console"] stream: ext://sys.stdout console_stat: @@ -42,18 +33,6 @@ handlers: <<: *console level: INFO_EXTRA - file: - class: musify.log.handlers.CurrentTimeRotatingFileHandler - level: DEBUG - formatter: extended - filters: ["file"] - filename: '_logs/{}.log' - encoding: "utf-8" - delay: True - when: "m" - interval: 2 - count: 30 - loggers: test: &logger level: DEBUG @@ -66,4 +45,4 @@ loggers: root: level: DEBUG - handlers: [console_debug, file] + handlers: [console_debug] diff --git a/tests/conftest.py b/tests/conftest.py index e862827b..176db7fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,6 @@ import types from collections import defaultdict from pathlib import Path -from typing import Any import pytest import yaml @@ -18,7 +17,7 @@ from musify.libraries.remote.core.enum import RemoteObjectType from musify.libraries.remote.spotify.api import SpotifyAPI from musify.libraries.remote.spotify.wrangle import SpotifyDataWrangler -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger from musify.types import UnitCollection from musify.utils import to_collection from tests.libraries.remote.core.utils import ALL_ITEM_TYPES @@ -41,18 +40,6 @@ def pytest_configure(config: pytest.Config): MusifyLogger.disable_bars = True MusifyLogger.compact = True - 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) - for formatter in log_config["formatters"].values(): # ensure ANSI colour codes in format are recognised formatter["format"] = formatter["format"].replace(r"\33", "\33") diff --git a/tests/core/base.py b/tests/core/base.py index da0b3a8f..c0fd8ff0 100644 --- a/tests/core/base.py +++ b/tests/core/base.py @@ -2,8 +2,8 @@ import pytest -from musify.core.base import MusifyItem -from musify.core.printer import PrettyPrinter +from musify.base import MusifyItem +from musify.printer import PrettyPrinter from tests.core.printer import PrettyPrinterTester diff --git a/tests/core/enum.py b/tests/core/enum.py index b7d31634..b8d94872 100644 --- a/tests/core/enum.py +++ b/tests/core/enum.py @@ -1,8 +1,9 @@ from abc import ABCMeta, abstractmethod from collections.abc import Container -from musify.core.base import MusifyObject -from musify.core.enum import MusifyEnum, Fields, TagField, ALL_FIELDS, Field +from musify.base import MusifyObject +from musify.enum import MusifyEnum +from musify.field import Fields, TagField, ALL_FIELDS, Field class EnumTester(metaclass=ABCMeta): diff --git a/tests/core/printer.py b/tests/core/printer.py index af232ab6..deb2a526 100644 --- a/tests/core/printer.py +++ b/tests/core/printer.py @@ -2,7 +2,7 @@ import re from abc import ABCMeta, abstractmethod -from musify.core.printer import PrettyPrinter +from musify.printer import PrettyPrinter class PrettyPrinterTester(metaclass=ABCMeta): diff --git a/tests/libraries/core/collection.py b/tests/libraries/core/collection.py index bc9368c5..26cc2898 100644 --- a/tests/libraries/core/collection.py +++ b/tests/libraries/core/collection.py @@ -6,8 +6,8 @@ import pytest -from musify.core.base import MusifyItem -from musify.core.printer import PrettyPrinter +from musify.base import MusifyItem +from musify.printer import PrettyPrinter from musify.exception import MusifyTypeError from musify.libraries.collection import BasicCollection from musify.libraries.core.collection import MusifyCollection diff --git a/tests/libraries/local/playlist/test_xautopf.py b/tests/libraries/local/playlist/test_xautopf.py index 7ff911d8..d9b40d23 100644 --- a/tests/libraries/local/playlist/test_xautopf.py +++ b/tests/libraries/local/playlist/test_xautopf.py @@ -7,7 +7,7 @@ import pytest -from musify.core.enum import Fields +from musify.field import Fields from musify.file.exception import InvalidFileType from musify.file.path_mapper import PathMapper, PathStemMapper from musify.libraries.local.library import MusicBee, LocalLibrary diff --git a/tests/libraries/local/track/test_track.py b/tests/libraries/local/track/test_track.py index 48d08b63..d9180cba 100644 --- a/tests/libraries/local/track/test_track.py +++ b/tests/libraries/local/track/test_track.py @@ -5,7 +5,7 @@ import mutagen import pytest -from musify.core.base import MusifyItem +from musify.base import MusifyItem from musify.exception import MusifyKeyError from musify.file.exception import InvalidFileType, FileDoesNotExistError from musify.file.image import open_image diff --git a/tests/libraries/remote/core/library.py b/tests/libraries/remote/core/library.py index 47e0b9aa..2bf63e0b 100644 --- a/tests/libraries/remote/core/library.py +++ b/tests/libraries/remote/core/library.py @@ -7,7 +7,7 @@ import pytest -from musify.core.base import MusifyItem +from musify.base import MusifyItem from musify.libraries.core.object import Playlist from musify.libraries.remote.core.library import RemoteLibrary from musify.libraries.remote.core.object import RemoteTrack diff --git a/tests/libraries/remote/core/processors/search.py b/tests/libraries/remote/core/processors/search.py index df206cbe..f0d74de0 100644 --- a/tests/libraries/remote/core/processors/search.py +++ b/tests/libraries/remote/core/processors/search.py @@ -5,8 +5,8 @@ import pytest -from musify.core.base import MusifyItemSettable -from musify.core.enum import TagFields as Tag +from musify.base import MusifyItemSettable +from musify.field import TagFields as Tag from musify.libraries.collection import BasicCollection from musify.libraries.core.collection import MusifyCollection from musify.libraries.core.object import Album diff --git a/tests/libraries/remote/spotify/test_processors.py b/tests/libraries/remote/spotify/test_processors.py index d61232df..186c0784 100644 --- a/tests/libraries/remote/spotify/test_processors.py +++ b/tests/libraries/remote/spotify/test_processors.py @@ -7,7 +7,7 @@ import pytest -from musify.core.enum import TagFields as Tag +from musify.field import TagFields as Tag from musify.exception import MusifyEnumError from musify.libraries.local.collection import LocalAlbum from musify.libraries.local.track import LocalTrack diff --git a/tests/log/__init__.py b/tests/log/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/log/test_handlers.py b/tests/log/test_handlers.py deleted file mode 100644 index 61afd267..00000000 --- a/tests/log/test_handlers.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -import string -from datetime import datetime, timedelta -from pathlib import Path -from random import choice - -import pytest - -from musify.log import LOGGING_DT_FORMAT -from musify.log.handlers import CurrentTimeRotatingFileHandler -from tests.utils import random_str - - -########################################################################### -## Logging handlers -########################################################################### -def test_current_time_file_handler_namer(tmp_path: Path): - # no filename given - handler = CurrentTimeRotatingFileHandler(delay=True) - assert handler.filename == Path(handler.dt.strftime(LOGGING_DT_FORMAT) + ".log") - - # no format part given - handler = CurrentTimeRotatingFileHandler(filename="test.log", delay=True) - assert handler.filename == Path("test.log") - - # filename with format part given and fixes separators - base_parts = [*str(tmp_path).split(os.path.sep), "folder", "file_{}_suffix.log"] - base_path = Path(*base_parts) - - handler = CurrentTimeRotatingFileHandler(filename=base_path, delay=True) - assert handler.filename == Path(str(base_path).format(handler.dt.strftime(LOGGING_DT_FORMAT))) - - sep = "\\" if os.path.sep == "/" else "/" - assert sep not in str(handler.filename) - - base_parts[-1] = base_parts[-1].format(handler.dt.strftime(LOGGING_DT_FORMAT)) - assert handler.filename.parts == tuple(p for p in base_parts if p) - - -@pytest.fixture -def log_paths(tmp_path: Path) -> list[Path]: - """Generate a set of log files and return their paths""" - dt_now = datetime.now() - - paths = [] - for i in range(1, 50, 2): - dt_str = (dt_now - timedelta(hours=i)).strftime(LOGGING_DT_FORMAT) - path = tmp_path.joinpath(dt_str + ".log") - paths.append(path) - - with open(path, "w") as f: - for _ in range(600): - f.write(choice(string.ascii_letters)) - - return paths - - -def test_current_time_file_handler_rotator_time(log_paths: list[str], tmp_path: Path): - filename = tmp_path.joinpath("{}.log") - handler = CurrentTimeRotatingFileHandler(filename=filename, when="h", interval=10) - - for dt in handler.removed: - assert dt < handler.dt - timedelta(hours=10) - for path in tmp_path.glob("*"): - dt = datetime.strptime(path.stem, LOGGING_DT_FORMAT) - assert dt >= handler.dt - timedelta(hours=10) - - -def test_current_time_file_handler_rotator_count(log_paths: list[str], tmp_path: Path): - filename = tmp_path.joinpath("{}.log") - CurrentTimeRotatingFileHandler(filename=filename, count=10) - assert len(list(tmp_path.glob("*"))) == 10 - - -def test_current_time_file_handler_rotator_combined(log_paths: list[str], tmp_path: Path): - filename = tmp_path.joinpath("{}.log") - handler = CurrentTimeRotatingFileHandler(filename=filename, when="h", interval=10, count=3) - - for path in tmp_path.glob("*"): - dt = datetime.strptime(path.stem, LOGGING_DT_FORMAT) - assert dt >= handler.dt - timedelta(hours=10) - - assert len(list(tmp_path.glob("*"))) <= 3 - - -def test_current_time_file_handler_rotator_folders(tmp_path: Path): - dt_now = datetime.now() - - paths = [] - for i in range(1, 50, 2): - dt_str = (dt_now - timedelta(hours=i)).strftime(LOGGING_DT_FORMAT) - path = tmp_path.joinpath(dt_str) - paths.append(path) - os.makedirs(path) - - if i > 10: # all folders with dt >10hrs will be empty - continue - - with open(path.joinpath(random_str() + ".txt"), "w") as f: - for _ in range(600): - f.write(choice(string.ascii_letters)) - - handler = CurrentTimeRotatingFileHandler(filename=tmp_path.joinpath("{}"), when="h", interval=20) - - for path in tmp_path.glob("*"): - dt = datetime.strptime(path.name, LOGGING_DT_FORMAT) - assert dt >= handler.dt - timedelta(hours=10) # deleted all empty folders >10hrs diff --git a/tests/processors/test_download.py b/tests/processors/test_download.py index cf84348c..e519eccf 100644 --- a/tests/processors/test_download.py +++ b/tests/processors/test_download.py @@ -5,7 +5,7 @@ from pytest_mock import MockerFixture from musify import MODULE_ROOT -from musify.core.enum import Fields +from musify.field import Fields from musify.libraries.collection import BasicCollection from musify.libraries.local.track import LocalTrack from musify.processors.download import ItemDownloadHelper diff --git a/tests/processors/test_filter.py b/tests/processors/test_filter.py index a4ae1711..81d39498 100644 --- a/tests/processors/test_filter.py +++ b/tests/processors/test_filter.py @@ -4,7 +4,7 @@ import pytest -from musify.core.enum import TagFields +from musify.field import TagFields from musify.file.path_mapper import PathStemMapper, PathMapper from musify.libraries.local.track import LocalTrack from musify.libraries.local.track.field import LocalTrackField diff --git a/tests/processors/test_match.py b/tests/processors/test_match.py index d911fe3c..25a4e1a3 100644 --- a/tests/processors/test_match.py +++ b/tests/processors/test_match.py @@ -1,6 +1,6 @@ import pytest -from musify.core.enum import TagFields as Tag +from musify.field import TagFields as Tag from musify.libraries.local.track import LocalTrack from musify.processors.match import ItemMatcher, CleanTagConfig from tests.core.printer import PrettyPrinterTester diff --git a/tests/log/test_logger.py b/tests/test_logger.py similarity index 75% rename from tests/log/test_logger.py rename to tests/test_logger.py index 4bce043b..0c2db2a1 100644 --- a/tests/log/test_logger.py +++ b/tests/test_logger.py @@ -4,9 +4,7 @@ import pytest -from musify.log import INFO_EXTRA, REPORT, STAT -from musify.log.filter import format_full_func_name, LogFileFilter -from musify.log.logger import MusifyLogger +from musify.logger import MusifyLogger, INFO_EXTRA, REPORT, STAT try: from tqdm.auto import tqdm @@ -188,54 +186,3 @@ def test_logger_set(): assert logging.getLoggerClass() == MusifyLogger assert isinstance(logging.getLogger(__name__), MusifyLogger) - - -########################################################################### -## Logging formatters/filters -########################################################################### -def test_format_func_name(): - record = logging.LogRecord( - name="this.is.a.short", - level=logging.INFO, - pathname=__name__, - lineno=10, - msg=None, - args=None, - exc_info=None, - func="path", - ) - format_full_func_name(record=record, width=20) - assert record.funcName == "this.is.a.short.path" - - record.name = "this.is.quite.a.long" - record.funcName = "path" - format_full_func_name(record=record, width=20) - assert record.funcName == "t.i.q.a.long.path" - - record.name = "this.path.has.a.ClassName" - record.funcName = "in_it" - format_full_func_name(record=record, width=20) - assert record.funcName == "t.p.h.a.CN.in_it" - - -def test_file_filter(): - log_filter = LogFileFilter(name="test") - record = logging.LogRecord( - name="test", - level=logging.INFO, - pathname=__name__, - lineno=10, - msg=None, - args=None, - exc_info=None, - func="function_name" - ) - - record.msg = "normal message" - log_filter.filter(record) - assert record.msg == "normal message" - - # noinspection SpellCheckingInspection - record.msg = "\33[91;1mcolour \33[94;0mmessage\33[0m" - log_filter.filter(record) - assert record.msg == "colour message" diff --git a/tests/utils.py b/tests/utils.py index aef895ed..ad0b223a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,7 +5,7 @@ from typing import Any from uuid import uuid4 -from musify.core.enum import MusifyEnum +from musify.enum import MusifyEnum path_tests = Path(__file__).parent path_root = path_tests.parent From 6b8d19133cad90b70f712f8ec3ce9fd4671dbb35 Mon Sep 17 00:00:00 2001 From: geo-martino Date: Tue, 11 Jun 2024 15:17:37 -0400 Subject: [PATCH 3/4] clean up code --- docs/_howto/scripts/sync/p1.py | 2 ++ musify/libraries/local/track/_tags/writer.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/_howto/scripts/sync/p1.py b/docs/_howto/scripts/sync/p1.py index 02197d44..8da712b9 100644 --- a/docs/_howto/scripts/sync/p1.py +++ b/docs/_howto/scripts/sync/p1.py @@ -1,3 +1,5 @@ +from p0 import * + from collections.abc import Collection from musify.libraries.core.collection import MusifyCollection diff --git a/musify/libraries/local/track/_tags/writer.py b/musify/libraries/local/track/_tags/writer.py index 8ade32d2..9ffbc82b 100644 --- a/musify/libraries/local/track/_tags/writer.py +++ b/musify/libraries/local/track/_tags/writer.py @@ -82,7 +82,7 @@ def _clear_tag(self, tag_name: str, dry_run: bool = True) -> bool: def clear_loaded_images(self) -> bool: """ Clear the loaded embedded images for this track. - Does not alter the actual file in anyway, only the loaded object in memory. + Does not alter the actual file in any way, only the loaded object in memory. """ tag_names = Tags.IMAGES.to_tag() removed = False From e39116225059e257bb76a9b2446507265e78b1aa Mon Sep 17 00:00:00 2001 From: geo-martino Date: Tue, 11 Jun 2024 15:19:49 -0400 Subject: [PATCH 4/4] update release history --- docs/release-history.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-history.rst b/docs/release-history.rst index 30b84a10..46473703 100644 --- a/docs/release-history.rst +++ b/docs/release-history.rst @@ -102,9 +102,7 @@ Changed * :py:meth:`.SpotifyAPI.create_playlist` now returns the full response rather than just the URL of the playlist. * Moved :py:class:`.RemoteItemChecker` and :py:class:`.RemoteItemSearcher` to `musify.processors` package. * Moved :py:class:`.RemoteDataWrangler` up a level to `musify.libraries.remote.core`. -* Deleted `musify.libraries.remote.core.processors` package. * Renamed `musify.libraries.remote.spotify.processors` module to `musify.libraries.remote.spotify.wrangle`. -* Removed logger filters and handlers. Moved to CLI repo. * Moved `musify.logger` module to `musify` base package. * Restructured contents of `musify.core` package to modules in `musify` base package. @@ -135,6 +133,8 @@ Removed * ThreadPoolExecutor use on :py:class:`.RemoteItemSearcher`. Now uses asynchronous logic instead. * `last_modified` field as attribute to ignore when getting attributes to print on `LocalCollection` to improve performance +* Removed logger filters and handlers. Moved to CLI repo. +* Deleted `musify.libraries.remote.core.processors` package. Documentation -------------