"""Utilities.""" from datetime import date, datetime, timedelta from io import BytesIO import time from typing import Callable, Iterable, List from download_spotify_charts import config from download_spotify_charts.constants import CSV_HEADER, CSV_NOTE, ChartBreakdown, ChartType def date_to_str(chart_date: date) -> str: """Date to str. Args: chart_date: Date. Returns: Date as str. """ return chart_date.strftime("%Y-%m-%d") def str_to_date(chart_date: str) -> date: """Str to date. Args: chart_date: Date as str. Returns: Date. """ return datetime.strptime(chart_date, "%Y-%m-%d").date() def get_default_date(chart_breakdown: ChartBreakdown) -> date: """Get default last date value. Args: chart_breakdown: Daily or weekly. Returns: Default date value. """ chart_date = datetime.utcnow().date() if chart_breakdown == ChartBreakdown.DAILY: return chart_date - timedelta(days=(config.DEFAULT_LAST_DATE_DIFF_DAILY + 1)) else: return chart_date - timedelta( days=(chart_date.weekday() + 7 * (config.DEFAULT_LAST_DATE_DIFF_WEEKLY + int(chart_date.weekday() < 5)) - 3) ) def get_csv_filename(chart_type: ChartType, chart_breakdown: ChartBreakdown, country_code: str, chart_date: date): """Get CSV file name. Args: chart_type: Regional or viral. chart_breakdown: Daily or weekly. country_code: Country code. chart_date: Chart date. Returns: Filename. """ return f"{chart_breakdown.value}/{chart_type.value}/{chart_date}/{country_code}.csv" def get_state_filename(chart_type: ChartType, chart_breakdown: ChartBreakdown) -> str: """Get last dates file name. Args: chart_breakdown: Daily or weekly. chart_type: Regional or viral. Returns: Filename. """ return f"config/{chart_breakdown.value}-{chart_type.value}.json" def put_row(io_mem: BytesIO, row: Iterable): """Put a new row to in-mem csv. Args: io_mem: In memory storage. row: Row items. """ row = ",".join(f'"{i}"' for i in row) + "\n" io_mem.write(row.encode()) def generate_row(chart_type: ChartType, data: dict) -> Iterable: """Generate row fields. Args: chart_type: Regional or viral. data: Data dict. Returns: Row fields. """ result = [ data["metrics"]["position"], data["public_meta"]["name"], ", ".join(i["name"] for i in data["public_meta"]["artists"]), ] if chart_type == ChartType.REGIONAL: result.append(data["metrics"]["date_streams"]) result.append(data["public_meta"]["uri"]) return result def data_to_csv(chart_type: ChartType, data: List[dict]) -> BytesIO: """Convert list of dicts to csv. Args: chart_type: Regional or viral. data: Data to put in csv. Returns: In memory csv. """ io_mem = BytesIO() if chart_type == ChartType.REGIONAL and config.PUT_NOTE_TO_CSV: put_row(io_mem, CSV_NOTE) if config.PUT_HEADER_TO_CSV: put_row(io_mem, CSV_HEADER[chart_type]) for row in sorted(data, key=lambda i: i["metrics"]["position"]): put_row(io_mem, generate_row(chart_type, row)) io_mem.seek(0) return io_mem def retry(count: int = config.DEFAULT_RETRY_COUNT, wait_rate: int = config.DEFAULT_RETRY_WAIT): """Retry decorator. Args: count: Retry count. wait_rate: Wait multiplier (current attempt number * wait_rate seconds). """ def inner(f: Callable): def wrapped(self, *args, **kwargs): for i in range(count + 1): try: return f(self, *args, **kwargs) except Exception as ex: self.logger.error(ex) if i == count: raise current_time = wait_rate * (i + 1) time.sleep(current_time) return wrapped return inner def generate_dates(chart_breakdown: ChartBreakdown, from_date: date, to_date: date) -> List[date]: """Generate dates range. Args: chart_breakdown: Daily or weekly. from_date: Date range from. to_date: Date range to. Returns: List of dates. """ if from_date > to_date: return [] step = 1 if chart_breakdown == ChartBreakdown.DAILY else 7 return [from_date + timedelta(days=i) for i in range(step, (to_date - from_date).days + 1, step)]