from datetime import date, datetime, timedelta import json from typing import Dict, Tuple from smelog.factory import BoundLogger from download_spotify_charts import config from download_spotify_charts import utils from download_spotify_charts.constants import ChartBreakdown, ChartType from download_spotify_charts.s3 import Client as S3Client class StateManager: def __init__( self, s3_client: S3Client, chart_type: ChartType, chart_breakdown: ChartBreakdown, logger: BoundLogger ): """Init state manager, load state from S3. Args: s3_client: S3 client. chart_breakdown: Daily or weekly. chart_type: Regional or viral. logger: Logger. """ self._s3_client = s3_client self._chart_type = chart_type self._chart_breakdown = chart_breakdown self._logger = logger self._is_changed = False self._state = self._load() @staticmethod def _get_item(last_date: date, as_str: bool = False) -> dict: """Generate state item. Args: last_date: Last date. as_str: Date as str. Returns: State item. """ return {"last_date": last_date.isoformat(), "ts": datetime.utcnow().isoformat()} def _load(self) -> Dict[str, dict]: """Load state (last run dates and timestamps). Returns: State data as country codes to last date and ts mapping. """ if config.BACKFILL_MODE: return {} return self._s3_client.get_json(utils.get_state_filename(self._chart_type, self._chart_breakdown), {}) @property def is_empty(self) -> bool: """Is empty or not. """ return not self._state @staticmethod def normalize_date(chart_breakdown: ChartBreakdown, chart_date: str or date or None) -> date or None: """Make thu date from str with possible -- and friday. Args: chart_breakdown: Daily or weekly. chart_date: Date, possibly as str, with --, friday. Returns: Normal date. """ if chart_date is None: return chart_date if chart_breakdown == ChartBreakdown.DAILY: return datetime.fromisoformat(chart_date).date() if isinstance(chart_date, str): if "--" in chart_date: chart_date = chart_date.split("--")[-1] chart_date = datetime.fromisoformat(chart_date).date() if chart_date.weekday() == 4: chart_date = chart_date - timedelta(days=1) return chart_date def get_min_date(self, chart_breakdown: ChartBreakdown) -> str or None: """Get min processed date. """ if self.is_empty: return None return self.normalize_date(chart_breakdown, min(i["last_date"] for i in self._state.values())) def get_country_item( self, chart_breakdown: ChartBreakdown, country_code: str, default_date: date ) -> Tuple[date, str or None]: """Get country item. Args: chart_breakdown: Daily or weekly. country_code: Country code. default_date: Default last date. Returns: Last date (as date) and timestamp (as str or None). """ country_item = self._state.get(country_code) if not country_item: country_item = self._get_item(default_date) last_date, ts = country_item["last_date"], country_item["ts"] return self.normalize_date(chart_breakdown, last_date), ts def set_country_item(self, country_code: str, last_date: date): """Set country item. Args: country_code: Country code. last_date: Last date. """ self._state[country_code] = self._get_item(last_date, True) self._is_changed = True def save(self): if self._is_changed: self._s3_client.put_json(utils.get_state_filename(self._chart_type, self._chart_breakdown), self._state) self._logger.debug(f"Config: {json.dumps(self._state)}") else: self._logger.debug(f"Data for {self._chart_breakdown.value} {self._chart_type.value} is up-to-date.")