""" Common utilities. """ from datetime import date, datetime, timedelta from typing import List from flask import request from sqlalchemy.exc import InvalidRequestError from sqlalchemy.orm.query import Query from core.constants import CLIENT_MOBILE, CLIENT_PORTAL, MARKET_GLOBAL, SPOTIFY_MARKET_GLOBAL from core.exceptions import JsonValidationError, QueryParamsValidationError, UnsupportedMediaType def get_request_json(schema=None): """ Parse and validate JSON from request. Args: schema (marshmallow.Schema): Optional Marshmallow schema for validating and cleaning the JSON. Returns: JSON parsed to native Python data structure. Raises: BadRequest: If request body cannot be parsed. JsonValidationError: If JSON does not pass schema validation. UnsupportedMediaType: If request media type is neither application/json nor application/*+json. """ if not request.is_json: raise UnsupportedMediaType('Unsupported media type "{}". Expected "application/json".'.format(request.mimetype)) if not schema: return request.get_json() data, errors = schema.load(request.get_json()) if errors: raise JsonValidationError(extra=errors) if data is None: # schema.load(None) does not return errors. # That is why this case is checked here. raise JsonValidationError() return data def fold_periods(dates, gap_size_close=1): """ Fold dates from list to list of periods [[date_from, date_to], [.., ..]]. Args: dates: List of dates. gap_size_close: it will close 1-day gaps by default Returns: List of periods pairs. """ result = [] if not dates: return result period_start, period_end = dates[0], dates[0] for i in range(1, len(dates)): if (dates[i] - dates[i - 1]).days <= gap_size_close + 1: period_end = dates[i] continue result.append([period_start, period_end]) period_start, period_end = dates[i], dates[i] result.append([period_start, period_end]) return result def sum_periods(dates: List[List[date]]) -> List[List[date]]: """Concat overlapping and consecutive date intervals. Args: dates (List[List[date]]): List of date pairs what are interval starts and ends. Returns: List[List[date]]: Aggregated list of date pairs. """ if not dates: return dates dates = sorted(dates, key=lambda x: (x[0], x[1])) prev_date = dates[0] results = [prev_date] for i in range(1, len(dates)): current_date = dates[i] if prev_date[1] >= (current_date[0] - timedelta(days=2)): if current_date[1] > prev_date[1]: prev_date[1] = current_date[1] else: prev_date = current_date results.append(prev_date) return results def get_client_onboarding_kwargs(client_type, user_id, create=False): """Build kwargs dictionary for onboarding query filtering and instance creating. Args: client_type (str): client type value user_id (str): user identifier create (bool): True - we will use kwargs for onboarding record creating, False - for filtering Returns: dict: Dictionary with fields values """ is_portal = client_type == CLIENT_PORTAL is_mobile = client_type == CLIENT_MOBILE result = {"user_id": user_id} update_dict = {} if create: update_dict = {"is_mobile": is_mobile, "is_portal": is_portal, "created_at": datetime.now()} elif is_portal: update_dict = {"is_portal": is_portal} elif is_mobile: update_dict = {"is_mobile": is_mobile} result.update(update_dict) return result def check_country_code_value(country_code: str or None) -> str or None: """ Check and fix country code value. Args: country_code (str or None): Country code value. Returns: (str or None): Country code value or None """ if not country_code: return None not_valid = len(country_code) != 2 and country_code not in (MARKET_GLOBAL, SPOTIFY_MARKET_GLOBAL) return None if not_valid else country_code class FilterMixin: """Mixin class for filtering query based on get parameters from request""" filter_serializer = None def filter_query(self, query: Query) -> Query: """Method for filtering query by get params from request""" if not hasattr(self, "filter_serializer"): return query data = self.filter_serializer().load_from_request() try: return query.filter_by(**data) except InvalidRequestError: raise QueryParamsValidationError