"""Bulk release class.""" import json from typing import Any import unicodedata from bulk_metadata_ingester_common.constants.model_csv_mapping \ import MODEL_CSV_MAPPING from bulk_metadata_ingester_common.models.release_model import ( ProductFieldsModel, TrackFieldsModel) # Class exceptions class ReleaseModelFailedException(Exception): """Release model fails exception.""" class TrackModelFailedException(Exception): """Track model fails exception.""" class ParticipantNotFoundException(Exception): """Participant not found exception.""" class BulkRelease: """Provides storage and methods for a bulk release.""" # Member vars _product: ProductFieldsModel _header: list field_separator: str = '\t' concat_seperator: str = '|' _tracks: dict _modified: dict def __init__(self, *args, json_release_rows: list, **kwargs): """Initialize the object. Args: json_release_rows (list): A list of rows from a bulk JSON document. Raises: ReleaseModelFailedException: No rows passed. ReleaseModelFailedException: Key / Value error. """ # Check if rows actually passed if not json_release_rows: raise ReleaseModelFailedException( 'Release could not be modeled: No rows passed.') # Get release-level remaps CSV_RELEASE_MAP = MODEL_CSV_MAPPING.get('release') # Get header header = list(json_release_rows[0].keys()) self._header = [h for h in header if h in CSV_RELEASE_MAP] try: # Map the incoming fields to the model. csv_release_row = \ {v: sanitize_string(json_release_rows[0][k]) for k, v in CSV_RELEASE_MAP.items()} # noqa: E501 # Attach product to object self._product = ProductFieldsModel(**csv_release_row) # Attach tracks self._tracks = {} for t in json_release_rows: self.add_track(t) except (KeyError, ValueError) as e: raise ReleaseModelFailedException( f'Release could not be modeled: {str(e)}') self._modified = {} def __str__(self) -> str: """Create a string representation. Returns: str: A JSON-compatible string representation of the model. """ track_dict = {} for k, t in self._tracks.items(): track_dict[k] = t.to_dict() release_dict = self._product._asdict() release_dict['tracks'] = track_dict return json.dumps(release_dict) def __getattr__(self, __name: str) -> Any: """Get a field.""" if __name == '_header': return self.__dict__['_header'] if __name == '_product': return self.__dict__['_product'] if __name == '_tracks' or __name == 'tracks': return self.__dict__['_tracks'] if __name == '_modified': return self.__dict__['_modified'] return getattr(self._product, __name) def __setattr__(self, __name: str, __value: Any) -> None: """Set a field.""" if __name == '_header': self.__dict__['_header'] = __value return if __name == '_product': self.__dict__['_product'] = __value return if __name == '_tracks' or __name == 'tracks': self.__dict__['_tracks'] = __value return if __name == '_modified': self.__dict__['_modified'] = __value return if not getattr(self._product, __name): self._product = self._product._replace(**{__name: __value}) self._modified[__name] = __value else: raise AttributeError( f"Can't set attribute '{__name}'. " f'Attribute has already been set.') def get_participant_by_name(self, name) -> list: """Return a participant object by name.""" try: pt = [p for p in self._product.participants if p['name'] == name][0] # noqa: E501 return pt except Exception as e: raise ParticipantNotFoundException( f'Participant {name} not found.') from e def add_track(self, csv_track_row: dict) -> None: """Add a track to the track list.""" t = BulkTrack(json_track_row=csv_track_row) # Create unique track key key = '_'.join([t.isrc, str(t.volume), str(t.track_no)]) # Attach track to dict self._tracks[key] = t def del_track(self, key) -> None: """Remove a track by key.""" del self._tracks[key] def to_dict(self) -> dict: """Return a dict representation.""" return json.loads(str(self)) def get_modified(self) -> dict: """Return a dict of all modified values.""" # Get all the changes for each track tracks = {} for k, t in self._tracks.items(): changes = t.get_modified() if changes: tracks[k] = changes # Shape tracks if present tracks = { 'tracks': tracks } # Combine release-level with track-level return { 'release': { **self._modified }, **tracks } def rehydrate(self, release_updates: dict, track_updates: dict) -> None: """Update values in the product. This will only update unset values. Args: release_updates (dict): A dict of key, value pairs to update on the release. track_updates (dict): A dict of key, value pairs to update on tracks. """ if release_updates: for k, v in release_updates.items(): self.__setattr__(k, v) if track_updates: for key, val in track_updates.items(): for k, v in val.items(): if represents_int(v): v = int(v) self._tracks[key].__setattr__(k, v) class BulkTrack: """Provides storage and methods for a bulk track.""" # Member vars _track: TrackFieldsModel = None _header: list _modified: dict def __init__(self, *args, json_track_row: dict, **kwargs): """Initialize the object. Args: csv_track_row (dict): a single row of data from a bulk JSON doc. header (list, optional): The header for the fields. Defaults to None. Raises: TrackModelFailedException: Track is not passed TrackModelFailedException: Bad key or value. """ CSV_TRACK_MAP = MODEL_CSV_MAPPING.get('track') # Get header header = list(json_track_row.keys()) self._header = [h for h in header if h in CSV_TRACK_MAP] if not json_track_row: raise TrackModelFailedException( 'Track could not be modeled: Row not passed.') try: # Map the incoming fields to the model. json_track_row = \ {v: sanitize_string(json_track_row[k]) for k, v in CSV_TRACK_MAP.items()} # noqa: E501 # Attach track to object self._track = TrackFieldsModel(**json_track_row) except (KeyError, ValueError) as e: raise TrackModelFailedException( f'Track could not be modeled: {str(e)}') self._modified = {} def __str__(self): """Create a string representation.""" track_dict = self._track._asdict() return json.dumps(track_dict) def __getattr__(self, __name: str) -> Any: """Get a field.""" if __name == '_header': return self.__dict__['_header'] if __name == '_track': return self.__dict__['_track'] if __name == '_modified': return self.__dict__['_modified'] return getattr(self._track, __name) def __setattr__(self, __name: str, __value: Any) -> None: """Set a field.""" if __name == '_header': self.__dict__['_header'] = __value return if __name == '_track': self.__dict__['_track'] = __value return if __name == '_modified': self.__dict__['_modified'] = __value return if not getattr(self._track, __name): self._track = self._track._replace(**{__name: __value}) self._modified[__name] = __value else: raise AttributeError( f"Can't set attribute '{__name}'. " f'Attribute has already been set.') def to_dict(self) -> dict: """Return a dict representation.""" return json.loads(str(self)) def get_modified(self) -> dict: """Return a dict of all modified values.""" return self._modified def represents_int(s): """Check if a str is an int.""" try: int(s) except ValueError: return False else: return True def main(): """Execute main.""" pass if __name__ == '__main__': main() def sanitize_string(s: str) -> str: """Sanitize string to remove control characters. All control characters start with a 'C' in unicode. Sanitization rules taken from: https://stackoverflow.com/questions/4324790/removing-control-characters-from-a-string-in-python # noqa: E501 Args: s (any): A possible string field. Returns: any: The given object OR a sanitized string. """ if s and isinstance(s, str): s = s.strip() return ''.join(ch for ch in s if unicodedata.category(ch)[0] != 'C') return s