"""Music Analytics Report.""" import functools import re from typing import IO def add_error(f): """Add error line to an error file, decorator.""" @functools.wraps(f) def write_error_row_to_file(self, *args, **kwargs): try: return f(self, *args, **kwargs) except Exception as ex: errors_file = self.open_errors_file() n = len(errors_file.readlines()) if not n: errors_file.write(self.header + '\treason\n') errstr = args[0] + f'\tError: {str(ex)}\n' errors_file.write(errstr), errors_file.close() return write_error_row_to_file class MusicAnalyticsReport: """MusicAnalytics report class to validate report data, and write files.""" def __init__(self, report_type, raw=None): """Initialise MusicAnalytics report.""" if raw: date = re.search(r'(\d{4})(-(\d{2}))+', raw) if date: self.date = date.group() self.report = ''.join([w.capitalize() for w in report_type.split('_')]) self.encoding = 'UTF-8' self.apparent_encoding = 'ascii' self.file_name_pattern = f'AMAnalytics_{self.report}' \ f'Report' + '_{date}.' self.file_formats = ['txt', 'csv'] self.not_null_cols = [] self.identifier = '' self.__raw = raw[:-1] if raw[-1] == '\n' else raw # noqa: F632 self.__header = raw.split('\n')[0] self.__errors_file = f'AMAnalytics_{self.report}' \ f'ReportErrors' + '_{date}.csv' self.columns = self.__header.split('\t') @property def header(self): """Return file header.""" return self.__header @staticmethod def get_report_by_type(report_type, raw): """Get a corresponding report by its type.""" reports = { 'in_review': InReviewReport, 'excluded_streams': ExcludedStreamsReport, } return reports.get(report_type)(raw) def open_errors_file(self, mode='r+'): """Open errors file. If doesn't exist, create it.""" self.__errors_file = self.__errors_file.format(date=self.date) try: return open(self.__errors_file, mode) except FileNotFoundError: return open(self.__errors_file, 'w+') @add_error def __validate_string(self, row): _validated = [] if row == self.__header: return None # do not return headers row_values = list(map( lambda v: None if v == 'null' else v, row.split('\t'))) for v, k in list(zip(row_values, self.columns)): if k in self.not_null_cols: if v is None: raise ValueError(f"The value of {k} shouldn't be None.") v = int(v) _validated.append(v) if len(_validated) == len(self.columns): return tuple(_validated) else: _num = '' for num, line in enumerate(self.__raw.split('\n'), 1): if row in line: _num = f'on line {num}' if num != '' else _num raise ValueError( f'Report misses values {_num} for a ' f'song with {self.identifier} ' f'{row_values[self.columns.index(self.identifier)]}.') def read_report_file(self, date, file_path, _format='csv') -> IO: """Read a local file; intended for local use.""" self.date = date if _format in self.file_formats: report_file = open( file_path + '/' + self.file_name_pattern. format(date=date) + _format, 'r') self.__raw = report_file.read() report_file.close() return report_file def write_to_report_file(self, data, _format='csv') -> str: """Open a file and write down the lines.""" if isinstance(data, str): data_string = data else: data_string = self.__header + '\n' + \ '\n'.join(['\t'.join(map(str, _tuple)) for _tuple in data]) # noqa if _format in self.file_formats: report_file = open( self.file_name_pattern.format(date=self.date) + _format, 'w+') report_file.write(data_string), report_file.close() return report_file.name # noqa def validate_report_data(self): """Validate report data.""" if self.__raw: lines = self.__raw.split('\n') else: raise Exception('No data is present. Provide either the existing ' 'report file date or raw data from API.') _v = self.__validate_string a = list(map(_v, filter(_v, lines))) return a def write_report_data_to_file(self): """Write a report and return also errors file if exists.""" file = dict(report=self.write_to_report_file(self.__raw)) self.validate_report_data() if self.date in self.__errors_file: file.update(errors=self.__errors_file) return file class InReviewReport(MusicAnalyticsReport): """In Review report class.""" def __init__(self, raw): """Initialise InReview report.""" super().__init__('in_review', raw) self.not_null_cols = ['song_id', 'streams'] self.identifier = 'song_id' class ExcludedStreamsReport(MusicAnalyticsReport): """Excluded Streams report class.""" def __init__(self, raw): """Initialise ExcludedStreams report.""" super().__init__('excluded_streams', raw) self.not_null_cols = ['apple_identifier', 'quantity_excluded'] self.identifier = 'apple_identifier'