diff --git a/docs/index.rst b/docs/index.rst index 3d12dbaf..a885d40e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -72,4 +72,5 @@ Install through pip using one of the following commands: musify.report musify.types musify.utils + genindex diff --git a/docs/release-history.rst b/docs/release-history.rst index b0a64afa..1fa246e3 100644 --- a/docs/release-history.rst +++ b/docs/release-history.rst @@ -43,34 +43,35 @@ Added * Introduced :py:class:`.MusifyItemSettable` class to allow distinction between items that can have their properties set and those that can't * Extend :py:class:`.FilterMatcher` with group_by tag functionality -* Now fully supports parsing of processors relating to XAutoPF objects with full I/O of settings - to/from their related XML files on disk. -* Now supports creating new XAutoPF files from scratch without the file needing to already exist. +* Now fully supports parsing of processors relating to :py:class:`.XAutoPF` objects with full I/O of settings + to/from their related XML files on disk +* Now supports creating new :py:class:`.XAutoPF` files from scratch without the file needing to already exist For XML values not directly controlled by Musify, users can use the 'default_xml' class attribute - to control the initial default values applied in this scenario. + to control the initial default values applied in this scenario Changed ------- * Major refactoring and restructuring to all modules to improve modularity and add composition -* :py:meth:`.LocalLibrary.load_tracks` and :py:meth:`.LocalLibrary.load_playlists` now run concurrently. +* :py:meth:`.LocalLibrary.load_tracks` and :py:meth:`.LocalLibrary.load_playlists` now run concurrently * Made :py:func:`.load_tracks` and :py:func:`.load_playlists` utility functions more DRY * Move :py:meth:`.TagReader.load` from :py:class:`.LocalTrack` to super class :py:class:`.TagReader` * :py:meth:`.SpotifyAPI.extend_items` now skips on responses that are already fully extended * :py:meth:`.SpotifyArtist.load` now uses the base `load` method from :py:class:`.SpotifyCollectionLoader` - meaning it now takes full advantage of the item filtering this method offers. + meaning it now takes full advantage of the item filtering this method offers As part of this, the base method was made more generic to accommodate all :py:class:`.SpotifyObject` types * Renamed 'kind' property on :py:class:`.LocalTrack` to 'type' to avoid clashing property names * :py:class:`.ItemMatcher`, :py:class:`.RemoteItemChecker`, and :py:class:`.RemoteItemSearcher` now accept - all MusifyItem types that may have their URI property set manually. + all MusifyItem types that may have their URI property set manually * :py:class:`.ItemSorter` now shuffles randomly on unsupported types + prioritises fields settings over shuffle settings * :py:meth:`.Comparer._in_range` now uses inclusive range i.e. ``a <= x <= b`` where ``x`` is the value to compare and ``a`` and ``b`` are the limits. Previously used exclusive range i.e. ``a < x < b`` * Removed ``from_xml`` and ``to_xml`` methods from all :py:class:`.MusicBeeProcessor` subclasses. - Moved this logic to :py:class:`.XMLPlaylistParser` as distinct 'get' methods for each processor type. + Moved this logic to :py:class:`.XMLPlaylistParser` as distinct 'get' methods for each processor type * Moved loading of XML file logic from :py:class:`.XAutoPF` to :py:class:`.XMLPlaylistParser`. - :py:class:`.XMLPlaylistParser` is now solely responsible for all XML parsing and handling for XAutoPF files + :py:class:`.XMLPlaylistParser` is now solely responsible for all XML parsing and handling + for :py:class:`.XAutoPF` files Fixed ----- @@ -81,6 +82,7 @@ Removed ------- * Redundant ShuffleBy enum and related arguments from :py:class:`.ItemSorter` +* `ItemProcessor` and `MusicBeeProcessor` abstraction layers. No longer needed after some refactoring Documentation ------------- @@ -94,7 +96,7 @@ Changed ------- * :py:class:`.ItemSorter` now accepts ``shuffle_weight`` between -1 and 1 instead of 0 and 1. - This parameter's logic has not yet been implemented so no changes to functionality have been made yet. + This parameter's logic has not yet been implemented so no changes to functionality have been made yet * Move :py:meth:`.get_filepaths` from :py:class:`.LocalTrack` to super class :py:class:`.File` Documentation @@ -109,13 +111,13 @@ Fixed * Tweaked assignment of description of IDv3 comment tags for :py:class:`.MP3` * :py:func:`.align_string` function now handles combining unicode characters properly for fixed-width fonts * :py:meth:`.LocalTrack.get_filepaths` on LocalTrack no longer returns paths from ``$RECYCLE.BIN`` folders. - These are deleted files and were causing the package to crash when trying to load them. + These are deleted files and were causing the package to crash when trying to load them * :py:meth:`.PrettyPrinter.json` and :py:meth:`.PrettyPrinter._to_str` converts attribute keys to string to ensure safe json/str/repr output * :py:class:`.FilterMatcher` and :py:class:`.FilterComparers` now correctly import conditions from XML playlist files. Previously, these filters could not import nested match conditions from files. - Changes to logic also made to :py:meth:`.Comparer.from_xml` to accommodate. -* :py:class:`.XMLLibraryParser` now handles empty arrays correctly. Previously would crash. + Changes to logic also made to :py:meth:`.Comparer.from_xml` to accommodate +* :py:class:`.XMLLibraryParser` now handles empty arrays correctly. Previously would crash * Fixed :py:class:`.Comparer` dynamic process method alternate names for ``in_the_last`` and ``not_in_the_last`` Removed @@ -137,29 +139,29 @@ Changed * Generating folders for a :py:class:`.LocalLibrary` now uses folder names as relative to the library folders of the :py:class:`.LocalLibrary`. - This now supports nested folder structures better. -* Writing date tags to :py:class:`.LocalTrack` now supports partial dates of only YYYY-MM. -* Writing date tags to :py:class:`.LocalTrack` skips writing year, month, day tags if date tag already written. + This now supports nested folder structures better +* Writing date tags to :py:class:`.LocalTrack` now supports partial dates of only YYYY-MM +* Writing date tags to :py:class:`.LocalTrack` skips writing year, month, day tags if date tag already written Removed ------- * set_compilation_tags method removed from :py:class:`.LocalFolder`. - This contained author specific logic and was not appropriate for general use. + This contained author specific logic and was not appropriate for general use Fixed ----- * ConnectionError catch in :py:class:`.RequestHandler` now handles correctly * Added safe characters and replacements for path conversion in MusicBee :py:class:`.XMLLibraryParser`. - Now converts path to expected XML format correctly. -* :py:class:`.FilterMatcher` now handles '&' character correctly. + Now converts path to expected XML format correctly +* :py:class:`.FilterMatcher` now handles '&' character correctly * :py:class:`.SpotifyAPI` now only requests batches of up to 20 items when getting albums. - Now matches Spotify Web API specifications better. + Now matches Spotify Web API specifications better * Loading of logging yaml config uses UTF-8 encoding now * Removed dependency on pytest-lazy-fixture. Package is `broken for pytest >8.0 `_. - Replaced functionality with forked version of code. + Replaced functionality with forked version of code 0.7.6 diff --git a/musify/api/authorise.py b/musify/api/authorise.py index 3906ac86..e78ddbfd 100644 --- a/musify/api/authorise.py +++ b/musify/api/authorise.py @@ -42,18 +42,21 @@ class APIAuthoriser: } :param name: The name of the API service being accessed. - :param auth_args: The parameters to be passed to the requests.post() function for initial token authorisation. - See description for possible example values. - :param user_args: Parameters to be passed to the requests.post() function - for requesting user authorised access to API services. + :param auth_args: The parameters to be passed to the + `requests.post() `_ + function for initial token authorisation. See description for possible example values. + :param user_args: The parameters to be passed to the + `requests.post() `_ + function for requesting user authorised access to API services. The code response from this request is then added to the authorisation request args - to grant user authorisation to the API. - See description for possible example values. - :param refresh_args: Parameters to be passed to the requests.post() function - for refreshing an expired token when a refresh_token is present. + to grant user authorisation to the API. See description for possible example values. + :param refresh_args: The parameters to be passed to the + `requests.post() `_ + function for refreshing an expired token when a refresh_token is present. See description for possible example values. - :param test_args: Parameters to be passed to the requests.get() function for testing validity of the token. - Must be set in conjunction with test_condition to work. + :param test_args: The parameters to be passed to the + `requests.get() `_ + function for testing validity of the token. Must be set in conjunction with test_condition to work. See description for possible example values. :param test_condition: Callable function for testing the response from the given test_args. e.g. ``lambda r: "error" not in r`` diff --git a/musify/field.py b/musify/field.py index 3ef254e9..f0b43657 100644 --- a/musify/field.py +++ b/musify/field.py @@ -21,7 +21,7 @@ def map(cls, enum: Self) -> list[Self]: class TrackField(TrackFieldMixin): - """Represent all currently supported fields for objects of type :py:class:`Track`""" + """Represents all currently supported fields for objects of type :py:class:`Track`""" ALL = TagFields.ALL.value TITLE = TagFields.TITLE.value @@ -49,7 +49,7 @@ class TrackField(TrackFieldMixin): class PlaylistField(Field): - """Represent all currently supported fields for objects of type :py:class:`Playlist`""" + """Represents all currently supported fields for objects of type :py:class:`Playlist`""" ALL = Fields.ALL.value # tags/core properties @@ -66,7 +66,7 @@ class PlaylistField(Field): class FolderField(Field): - """Represent all currently supported fields for objects of type :py:class:`Folder`""" + """Represents all currently supported fields for objects of type :py:class:`Folder`""" ALL = Fields.ALL.value # tags/core properties @@ -81,7 +81,7 @@ class FolderField(Field): class AlbumField(Field): - """Represent all currently supported fields for objects of type :py:class:`Album`""" + """Represents all currently supported fields for objects of type :py:class:`Album`""" ALL = Fields.ALL.value # tags/core properties @@ -102,7 +102,7 @@ class AlbumField(Field): class ArtistField(Field): - """Represent all currently supported fields for objects of type :py:class:`Artist`""" + """Represents all currently supported fields for objects of type :py:class:`Artist`""" ALL = Fields.ALL.value # tags/core properties diff --git a/musify/libraries/local/playlist/xautopf.py b/musify/libraries/local/playlist/xautopf.py index 25a60190..21420103 100644 --- a/musify/libraries/local/playlist/xautopf.py +++ b/musify/libraries/local/playlist/xautopf.py @@ -19,7 +19,7 @@ from musify.libraries.local.playlist.base import LocalPlaylist from musify.libraries.local.track import LocalTrack from musify.processors.compare import Comparer -from musify.processors.exception import ItemSorterError +from musify.processors.exception import SorterProcessorError from musify.processors.filter import FilterDefinedList, FilterComparers from musify.processors.filter_matcher import FilterMatcher from musify.processors.limit import ItemLimiter, LimitType @@ -78,6 +78,10 @@ class XAutoPF(LocalPlaylist[AutoMatcher]): __slots__ = ("_parser",) valid_extensions = frozenset({".xautopf"}) + + #: The initial values to use as the base XML when creating a new XAutoPF file from scratch. + #: Certain settings in the 'Source' key relating to processors that this program recognises + #: will be set on initialisation. Hence, any default values assigned to these keys will be overridden. default_xml = { "SmartPlaylist": { "@SaveStaticCopy": "False", @@ -183,7 +187,7 @@ class XMLPlaylistParser(File, PrettyPrinter): __slots__ = ("_path", "path_mapper", "xml",) # noinspection SpellCheckingInspection - #: Map of MusicBee field name to Field enum + #: Map of MusicBee field key name to Field enum name_field_map = { "None": None, "Title": Fields.TITLE, @@ -220,6 +224,7 @@ class XMLPlaylistParser(File, PrettyPrinter): "FileLastPlayed": Fields.LAST_PLAYED, "FilePlayCount": Fields.PLAY_COUNT, } + #: Map of Field enum to MusicBee field key name field_name_map = {field: name for name, field in name_field_map.items()} #: Settings for custom sort codes. @@ -233,7 +238,9 @@ class XMLPlaylistParser(File, PrettyPrinter): # TODO: implement field_code 78 - manual order according to the order of tracks found # in the MusicBee library file for a given playlist. } + #: The default/manual sort code default_sort = 78 + #: The default setting to use for the GroupBy setting default_group_by = "track" @property @@ -265,8 +272,8 @@ def description(self, value: str | None): def __init__(self, path: str, path_mapper: PathMapper = PathMapper()): self._path = path + #: Maps paths stored in the playlist file. self.path_mapper = path_mapper - #: A map representation of the loaded XML playlist data self.xml: dict[str, Any] = {} @@ -529,7 +536,7 @@ def get_sorter(self) -> ItemSorter | None: elif "DefinedSort" in self.xml_source: fields = [field] else: - raise ItemSorterError("Sort type in XML not recognised") + raise SorterProcessorError("Sort type in XML not recognised") shuffle_mode_value = self._pascal_to_snake(self.xml_smart_playlist["@ShuffleMode"]) if not fields and shuffle_mode_value != "none": @@ -560,7 +567,7 @@ def parse_sorter(self, sorter: ItemSorter | None = None) -> None: return if len(sorter.sort_fields) > 1: - raise ItemSorterError( + raise SorterProcessorError( "Cannot generate an XML representation of a mapping of many sort fields unless they have " "a defined sort ID. To parse these fields to XML, define this map in the 'defined_sort' " f"class attribute of this parser | {sorter.sort_fields}" diff --git a/musify/libraries/local/track/field.py b/musify/libraries/local/track/field.py index 4b82e6fb..ed701cc1 100644 --- a/musify/libraries/local/track/field.py +++ b/musify/libraries/local/track/field.py @@ -6,7 +6,7 @@ class LocalTrackField(TrackFieldMixin): - """Represent all currently supported fields for objects of type :py:class:`LocalTrack`""" + """Represents all currently supported fields for objects of type :py:class:`LocalTrack`""" ALL = TagFields.ALL.value TITLE = TagFields.TITLE.value diff --git a/musify/libraries/remote/core/api.py b/musify/libraries/remote/core/api.py index aea7256c..a6f0016d 100644 --- a/musify/libraries/remote/core/api.py +++ b/musify/libraries/remote/core/api.py @@ -310,7 +310,7 @@ def get_items( use_cache: bool = True, ) -> list[dict[str, Any]]: """ - ``GET: /{kind}s`` - Get information for given ``values``. + ``GET`` - Get information for given ``values``. ``values`` may be: * A string representing a URL/URI/ID. diff --git a/musify/processors/base.py b/musify/processors/base.py index c535deca..03bb5724 100644 --- a/musify/processors/base.py +++ b/musify/processors/base.py @@ -49,19 +49,6 @@ def _format_help_text(options: Mapping[str, str], header: MutableSequence[str] | return "\n\t".join(help_text) + '\n' -class ItemProcessor(Processor, metaclass=ABCMeta): - """Base object for processing :py:class:`MusifyItem` objects""" - - -class MusicBeeProcessor(ItemProcessor, metaclass=ABCMeta): - """Base object for processing :py:class:`MusifyItem` objects on MusicBee settings""" - - @classmethod - def _processor_method_fmt(cls, name: str) -> str: - """A custom formatter to apply to the dynamic processor name""" - return "_" + cls._pascal_to_snake(name) - - # noinspection PyPep8Naming,SpellCheckingInspection class dynamicprocessormethod: """ @@ -115,6 +102,11 @@ def processor_methods(self) -> frozenset[str]: """String representation of the current processor name of this object""" return frozenset(self._processor_method_fmt(name) for name in self.__processormethods__) + @classmethod + def _processor_method_fmt(cls, name: str) -> str: + """A custom formatter to apply to the dynamic processor name""" + return name + def __new__(cls, *_, **__): processor_methods = list(cls.__processormethods__) @@ -138,11 +130,6 @@ def __new__(cls, *_, **__): def __init__(self): self._processor_name: str | None = None - @classmethod - def _processor_method_fmt(cls, name: str) -> str: - """A custom formatter to apply to the dynamic processor name""" - return name - def _set_processor_name(self, value: str | None, fail_on_empty: bool = True): """Verifies and sets the condition name""" if value is None: diff --git a/musify/processors/compare.py b/musify/processors/compare.py index 20669a8c..e79f524a 100644 --- a/musify/processors/compare.py +++ b/musify/processors/compare.py @@ -10,14 +10,14 @@ from musify.core.base import MusifyItem from musify.core.enum import Field -from musify.processors.base import DynamicProcessor, MusicBeeProcessor, dynamicprocessormethod +from musify.processors.base import DynamicProcessor, dynamicprocessormethod from musify.processors.exception import ComparerError from musify.processors.time import TimeMapper from musify.types import UnitSequence from musify.utils import to_collection -class Comparer(MusicBeeProcessor, DynamicProcessor): +class Comparer(DynamicProcessor): """ Compares an item or object with another item, object or a given set of expected values to find a match. @@ -31,6 +31,10 @@ class Comparer(MusicBeeProcessor, DynamicProcessor): __slots__ = ("_expected", "_converted", "field") + @classmethod + def _processor_method_fmt(cls, name: str) -> str: + return "_" + cls._pascal_to_snake(name) + @property def condition(self) -> str: """String representation of the current condition name of this object""" diff --git a/musify/processors/download.py b/musify/processors/download.py index 875127bc..c2dd4e4f 100644 --- a/musify/processors/download.py +++ b/musify/processors/download.py @@ -11,12 +11,12 @@ from musify.core.enum import Field, Fields from musify.exception import MusifyEnumError from musify.libraries.core.collection import MusifyCollection -from musify.processors.base import InputProcessor, ItemProcessor +from musify.processors.base import InputProcessor from musify.types import UnitIterable from musify.utils import to_collection -class ItemDownloadHelper(InputProcessor, ItemProcessor): +class ItemDownloadHelper(InputProcessor): """ Runs operations for helping the user to download items from given collections. diff --git a/musify/processors/exception.py b/musify/processors/exception.py index 9d3a5605..3161a1ef 100644 --- a/musify/processors/exception.py +++ b/musify/processors/exception.py @@ -16,6 +16,18 @@ class ComparerError(ProcessorError): """Exception raised for errors related to :py:class:`Comparer` settings.""" +class LimiterProcessorError(ProcessorError): + """Exception raised for errors related to :py:class:`ItemLimiter` logic.""" + + +class MatcherProcessorError(ProcessorError): + """Exception raised for errors related to :py:class:`ItemMatcher` logic.""" + + +class SorterProcessorError(ProcessorError): + """Exception raised for errors related to :py:class:`ItemSorter` logic.""" + + class TimeMapperError(ProcessorError): """Exception raised for errors related to :py:class:`TimeMapper` logic.""" @@ -25,22 +37,3 @@ class TimeMapperError(ProcessorError): ########################################################################### class FilterError(ProcessorError): """Exception raised for errors related to :py:class:`Filter` logic.""" - - -########################################################################### -## Item processor errors -########################################################################### -class ItemProcessorError(ProcessorError): - """Exception raised for errors related to :py:class:`ItemProcessor` logic.""" - - -class ItemLimiterError(ItemProcessorError): - """Exception raised for errors related to :py:class:`ItemLimiter` logic.""" - - -class ItemMatcherError(ItemProcessorError): - """Exception raised for errors related to :py:class:`ItemMatcher` logic.""" - - -class ItemSorterError(ItemProcessorError): - """Exception raised for errors related to :py:class:`ItemSorter` logic.""" diff --git a/musify/processors/filter_matcher.py b/musify/processors/filter_matcher.py index da1615ef..f19a78b7 100644 --- a/musify/processors/filter_matcher.py +++ b/musify/processors/filter_matcher.py @@ -11,7 +11,7 @@ from musify.core.enum import TagField from musify.core.result import Result from musify.log.logger import MusifyLogger -from musify.processors.base import Filter, MusicBeeProcessor, FilterComposite +from musify.processors.base import Filter, FilterComposite from musify.processors.filter import FilterComparers, FilterDefinedList @@ -24,7 +24,7 @@ class MatchResult[T: Any](Result): excluded: Collection[T] = field(default=tuple()) #: Objects that matched :py:class:`Comparer` settings compared: Collection[T] = field(default=tuple()) - #: Objects that matched on any group_by settings + #: Objects that matched on any ``group_by`` settings grouped: Collection[T] = field(default=tuple()) @property @@ -33,7 +33,7 @@ def combined(self) -> list[T]: return [track for track in [*self.compared, *self.included, *self.grouped] if track not in self.excluded] -class FilterMatcher[T: Any, U: Filter, V: Filter, X: FilterComparers](MusicBeeProcessor, FilterComposite[T]): +class FilterMatcher[T: Any, U: Filter, V: Filter, X: FilterComparers](FilterComposite[T]): """ Get matches for items based on given filters. diff --git a/musify/processors/limit.py b/musify/processors/limit.py index b0c2bb33..40da9891 100644 --- a/musify/processors/limit.py +++ b/musify/processors/limit.py @@ -10,8 +10,8 @@ from musify.core.enum import MusifyEnum, Fields from musify.file.base import File from musify.libraries.core.object import Track -from musify.processors.base import DynamicProcessor, MusicBeeProcessor, dynamicprocessormethod -from musify.processors.exception import ItemLimiterError +from musify.processors.base import DynamicProcessor, dynamicprocessormethod +from musify.processors.exception import LimiterProcessorError from musify.processors.sort import ItemSorter @@ -33,7 +33,7 @@ class LimitType(MusifyEnum): TERABYTES = 24 -class ItemLimiter(MusicBeeProcessor, DynamicProcessor): +class ItemLimiter(DynamicProcessor): """ Sort items in-place based on given conditions. @@ -52,6 +52,10 @@ class ItemLimiter(MusicBeeProcessor, DynamicProcessor): __slots__ = ("limit_max", "kind", "allowance") + @classmethod + def _processor_method_fmt(cls, name: str) -> str: + return "_" + cls._pascal_to_snake(name) + @property def limit_sort(self) -> str | None: """String representation of the sorting method to use before limiting""" @@ -114,7 +118,7 @@ def _limit_on_albums[T: MusifyItem](self, items: list[T]) -> list[T]: for item in items: if not isinstance(item, Track): - ItemLimiterError("In order to limit on Album, all items must be of type 'Track'") + LimiterProcessorError("In order to limit on Album, all items must be of type 'Track'") if len(seen_albums) < self.limit_max and item.album not in seen_albums: # album limit not yet reached @@ -146,7 +150,7 @@ def _convert(self, item: MusifyItem) -> float: """ if 10 < self.kind.value < 20: if not hasattr(item, "length"): - raise ItemLimiterError("The given item cannot be limited on length as it does not have a length.") + raise LimiterProcessorError("The given item cannot be limited on length as it does not have a length.") factors = (1, 60, 60, 24, 7)[:self.kind.value % 10] # noinspection PyUnresolvedReferences @@ -154,13 +158,13 @@ def _convert(self, item: MusifyItem) -> float: elif 20 <= self.kind.value < 30: if not isinstance(item, File): - raise ItemLimiterError("The given item cannot be limited on bytes as it is not a file.") + raise LimiterProcessorError("The given item cannot be limited on bytes as it is not a file.") bytes_scale = 1000 return item.size / (bytes_scale ** (self.kind.value % 10)) else: - raise ItemLimiterError(f"Unrecognised LimitType: {self.kind}") + raise LimiterProcessorError(f"Unrecognised LimitType: {self.kind}") @dynamicprocessormethod def _random(self, items: list[MusifyItem]) -> None: diff --git a/musify/processors/match.py b/musify/processors/match.py index 0fb600b6..7bcb3b12 100644 --- a/musify/processors/match.py +++ b/musify/processors/match.py @@ -13,7 +13,7 @@ from musify.core.printer import PrettyPrinter from musify.libraries.core.collection import MusifyCollection from musify.log.logger import MusifyLogger -from musify.processors.base import ItemProcessor +from musify.processors.base import Processor from musify.types import UnitIterable from musify.utils import limit_value, to_collection @@ -39,7 +39,7 @@ def as_dict(self) -> dict[str, Any]: } -class ItemMatcher(ItemProcessor): +class ItemMatcher(Processor): """Matches source items/collections to given result(s).""" __slots__ = ("logger",) diff --git a/musify/processors/sort.py b/musify/processors/sort.py index b822d3a8..72d2fbd1 100644 --- a/musify/processors/sort.py +++ b/musify/processors/sort.py @@ -9,7 +9,7 @@ from musify.core.base import MusifyItem from musify.core.enum import MusifyEnum, Field -from musify.processors.base import MusicBeeProcessor +from musify.processors.base import Processor from musify.types import UnitSequence, UnitIterable from musify.utils import flatten_nested, strip_ignore_words, to_collection, limit_value @@ -22,7 +22,7 @@ class ShuffleMode(MusifyEnum): DIFFERENT_ARTIST = 3 -class ItemSorter(MusicBeeProcessor): +class ItemSorter(Processor): """ Sort items in-place based on given conditions.