import copy from apollo_utils.core.utils.ext_enum import ExtEnum from collections import defaultdict from datetime import date, datetime, timedelta from functools import partial from operator import add from typing import Any, Callable, Dict, Iterable, List, Tuple, Union from server.constants.common import ABSENT from server.utils.mappers.core.getters import getter def get_inner_value(item: dict, *args, default_value: Any = None, convert_func: Callable = None) -> Any: for field in args: if item is None: return default_value if isinstance(item, (list, tuple, set)): item = item[field] if item else default_value else: item = item.get(field) if item is not None and convert_func: return convert_func(item) return item add1 = partial(add, 1) def is_none(x, none=None): return x is none def filter_by_prefixes( values: Iterable[str], prefixes: Iterable[str], delimiter: str = "_", trim_prefix: bool = False ) -> List[str]: result = [] for value in values: value_parts = value.split(delimiter) if len(value_parts) > 1 and value_parts[0] in prefixes: result.append(value[len(value_parts[0]) + len(delimiter) :] if trim_prefix else value) return result def prefix_str(v: str, prefix: str = None, delimiter=".") -> str: if is_none(prefix): return v return f"{prefix}{delimiter}{v}" def no_prefix_str(v: str, delimiter: str = ".", remove_idx=(0,)) -> str: v_parts = v.split(delimiter) return delimiter.join([v_parts[i] for i in range(len(v_parts)) if i not in remove_idx]) def unpack_enum_list( enum_list: List[Union[ExtEnum, str]], allow_str: bool = False, return_default: Any = None ) -> Union[List[str], None]: """Unpack list of ExtEnum and return list of ExtEnum.values or return return_default Args: enum_list: Iterable of objects allow_str: allows string values inside enum_list and adds value as is return_default: default value to return if not enum_list Returns: List of passed ExtEnum obj values Example: A: enum_list = [, ] unpack_enum_list(enum_list) -> ['playlists', 'tracks'] B: enum_list = [, 'fr'] unpack_enum_list(enum_list) -> TypeError("String values are not allowed") unpack_enum_list(enum_list, allow_str=True) -> ['us', 'fr'] C: enum_list = [] unpack_enum_list(enum_list) -> None (return_default: Any = None) """ if not enum_list: return return_default response = [] for i in enum_list: if isinstance(i, ExtEnum): response.append(i.value) continue if isinstance(i, str) and not allow_str: raise TypeError("String values are not allowed") response.append(i) return response async def group_by_items( items: List[Dict[str, Any]], item_group_by_key: str, item_meta_identifier_key: str, meta_identifier: Any, add_key: str, ) -> List[Dict[str, Any]]: """Group by list of dicts by identifier with main meta item and add all others items to meta item by passed key Args: items: item_group_by_key: Dict key of an item to group items by item_meta_identifier_key: Dict key to identify a meta item meta_identifier: a value of a field to be identified as a meta item (if such an item did not apper than the first item will be main meta) add_key: key to add all items as a list to meta items Example: We can grop by the next "items" by "playlist_id" and as a meta check will be "playlist.country_code" field Grouped by items will add to the main meta Dict item to a "grouped_by_playlist_id" field as a list of items Pay attention that first two items are the ones that will be grouped by, The first item will be mapped first (not a meta item with playlist.country_code=fr) But them it will be replaced by the 2-nd item (needs to be a meta item playlist.country_code=us) and the first item will be just added to add_key="grouped_by_playlist_id" List of items List of item that need to be grouped by: items = [ { "playlist_id": "spotify_02fBipHI4Vu46EfbB78rPD", "playlist": { "country_code": "fr", "name": "NOT!!!!!!!US META PLAYLIST NAME", }, }, { "playlist_id": "spotify_02fBipHI4Vu46EfbB78rPD", "playlist": { "country_code": "us", "name": "US META PLAYLIST NAME", }, }, { "playlist_id": "spotify_0B618tRUBzjUG7bhXxK8Ez", "playlist": { "country_code": "fr", "name": "Tik Tok Songs", }, }, ] Apply function on the items: grouped = await group_by_items( items=items, item_group_by_key="playlist_id", item_meta_identifier_key="playlist.country_code", meta_identifier="us", add_key="grouped_by_playlist_id" ) Final grouped by response grouped = [ { "playlist_id":"spotify_02fBipHI4Vu46EfbB78rPD", "playlist":{ "country_code":"us", "name":"US META PLAYLIST NAME" }, "grouped_by_playlist_id":[ { "playlist_id":"spotify_02fBipHI4Vu46EfbB78rPD", "playlist":{ "country_code":"fr", "name":"NOT!!!!!!!US META PLAYLIST NAME" } }, { "playlist_id":"spotify_02fBipHI4Vu46EfbB78rPD", "playlist":{ "country_code":"us", "name":"US META PLAYLIST NAME" } } ] }, { "playlist_id":"spotify_0B618tRUBzjUG7bhXxK8Ez", "playlist":{ "country_code":"fr", "name":"Tik Tok Songs" }, "grouped_by_playlist_id":[ { "playlist_id":"spotify_0B618tRUBzjUG7bhXxK8Ez", "playlist":{ "country_code":"fr", "name":"Tik Tok Songs" } } ] } ] """ if not items: return [] mapping = defaultdict(dict) for looped_item in items: looped_item_group_by_key = getter(looped_item, item_group_by_key, default_result=None) if not looped_item_group_by_key: continue looped_item_meta_identifier = getter(looped_item, item_meta_identifier_key, default_result=None) looped_item_is_meta_check = looped_item_meta_identifier == meta_identifier meta_or_first_looped_item = copy.deepcopy(looped_item) if looped_item_group_by_key not in mapping.keys(): looped_item[add_key] = [meta_or_first_looped_item] mapping[looped_item_group_by_key] = looped_item continue if looped_item_is_meta_check: add_key_value = mapping[looped_item_group_by_key].pop(add_key) add_key_value.append(meta_or_first_looped_item) looped_item[add_key] = add_key_value mapping[looped_item_group_by_key] = looped_item continue mapping[looped_item_group_by_key][add_key].append(looped_item) return list(mapping.values()) def extend_sorting_dict_mapper_with_opposite_key_value_pairs(original_dict: Dict[str, str], prefix: str = "-"): """Get original dict with sorting parameters mapping and extend it with opposite key value pairs Args: original_dict: Dict with str keys and values prefix: desc prefix value Example: a = { "-a": "-a", "b": "b", "c": "c", "-c": "-c" } extend_dict_with_asc_desc_key_value_pairs(a) a = { "-a": "-a", extended with -> "a": "a", "b": "b", extended with -> "-b": "-b", "c": "c", "-c": "-c" } """ extended_dict = dict() def add_to_dict(key, value, dict_to_update): dict_to_update[key] = value def check_opposite_key(key, dict_to_update): return key in dict_to_update.keys() def get_opposite(value: str): if value.startswith(prefix): return value[1:] return f"{prefix}{value}" for k, v in original_dict.items(): opposite_key = get_opposite(k) if check_opposite_key(opposite_key, original_dict): continue add_to_dict(opposite_key, get_opposite(v), extended_dict) original_dict.update(extended_dict) def to_dict( key_name: str, values: Iterable[dict], value_name: str = None, raise_no_key: bool = False, key_name_func: Callable = None, ) -> Dict[str, Any]: """ Transform Iterable of dicts to mapping. @param key_name: name which values to be taken by in original data are being used as a keys in a result map. @param values: original iterable of dictionaries. @param value_name: set if you want to map to a specific value only, check examples. @param raise_no_key: raise exception if there is no value by 'key_name' @param key_name_func: transform function for keys. """ d = {} for v in values: try: key_value = v.get(key_name, ABSENT) if key_value is ABSENT: continue if key_name_func: key_value = key_name_func(key_value) d[key_value] = v.get(value_name) if value_name else v except KeyError as e: if raise_no_key: raise e return d def conditional_tuple(*args, rules: Iterable[Tuple[Any, bool]] = None) -> tuple: rules = rules or [] result = list(args) for rule in rules: if rule[1]: result.append(rule[0]) return tuple(result) def get_included(data: dict, include: Iterable[str]) -> dict: return {k: v for k, v in data.items() if k in include} def fill_skipped_ends_for_dates( items: List[Any], request_dates=Tuple[date, date], response_dates=Tuple[date, date], default: Any = None, nones_as_default: tuple = None, ) -> List[Any]: if nones_as_default: items = [i if i not in nones_as_default else default for i in items] if request_dates == response_dates: return items if response_dates[0] < request_dates[0]: # first requested date > latest available return [default] * ((request_dates[1] - request_dates[0]).days + 1) return ( [default] * (response_dates[0] - request_dates[0]).days + items + [default] * (request_dates[1] - response_dates[1]).days ) def get_last_date_by_weekday(weekday: int) -> date: """Calc last week day date. Args: weekday: Week day number (from 0). Returns: date. """ last_date = datetime.utcnow().date() if last_date.weekday() <= weekday: last_date -= timedelta(weeks=1) return last_date + timedelta(days=weekday - last_date.weekday())