import asyncio import inspect import itertools from typing import Any, Callable, Dict, Iterable, List, Optional from server.utils.args.getter_setter import get_arg, set_arg def is_iterable(value: Any) -> bool: """Check if value is iterable except string. Args: value: Value to check. Returns: Is iterable or not. """ return isinstance(value, (list, tuple, set)) def request_per_item( field_name_list: str or List[str], default_value: list = None, result_cls: Callable = list, sum_func: Callable[[Any, Any, Iterable[Any], Dict[str, Any]], Any] = None, sum_all_func: Callable = None, modify_items_func: Callable = None, single_item_call: bool = False, filter_kwargs: bool = False, enable_arg_name: Optional[str] = None, ): """Call function for each item of some list argument. Args: field_name_list: List of list args fields names. default_value: Default field value if None (for cases when None = ALL). result_cls: Class to use result_cls() as initial result value to sum up. sum_func: A function to sum up result's chunks. sum_all_func: A function to sum up all result's chunks at once. modify_items_func: A function to modify field value (ex: request related ISRC list). single_item_call: Call a function with single item, not a new list of this single item. filter_kwargs: Filter extra kwargs using argspec. enable_arg_name: Bypass or not request per item flag field name. """ if is_iterable(field_name_list): if default_value: raise ValueError("Default value can not be used with multiple field names.") else: field_name_list = [field_name_list] def inner(f: Callable): args_spec = inspect.getfullargspec(f) async def wrapped(*args, **kwargs) -> List: if enable_arg_name and enable_arg_name in kwargs and kwargs.pop(enable_arg_name) is False: return await f(*args, **kwargs) field_mapping = {} for field_name in field_name_list: field_value = get_arg(args, kwargs, args_spec, field_name, from_default=False) or default_value if not field_value: continue if not is_iterable(field_value): field_value = [field_value] field_mapping[field_name] = field_value if not field_mapping: return await f(*args, **kwargs) if modify_items_func: for field_name, field_value in field_mapping.items(): field_mapping[field_name] = await modify_items_func(field_value, field_name, *args, **kwargs) tasks, args_and_kwargs = [], [] for item in itertools.product(*field_mapping.values()): modified_args, modified_kwargs = args, kwargs for field_name, field_value in zip(field_mapping.keys(), item): modified_args, modified_kwargs = set_arg( modified_args, modified_kwargs, args_spec, field_name, field_value if single_item_call else [field_value], filter_kwargs=filter_kwargs, ) args_and_kwargs.append((modified_args, modified_kwargs)) tasks.append(f(*modified_args, **modified_kwargs)) responses = await asyncio.gather(*tasks) if sum_all_func: result = sum_all_func(responses, args_spec, *args, **kwargs) else: result = result_cls() for i, chunk in enumerate(responses): chunk_args_kwargs = args_and_kwargs[i] result = ( sum_func(result, chunk, chunk_args_kwargs[0], chunk_args_kwargs[1]) if sum_func else result + chunk ) return result wrapped.__signature__ = inspect.signature(f) return wrapped return inner def request_per_item_many( fields: str or Iterable[str], result_cls: Callable = list, sum_func: Callable[[Any, Any, Iterable[Any], Dict[str, Any]], Any] = None, single_item_call: bool = False, ): """Call function for each item of some list argument. Uses first not empty argument from the list of field names, if all = None then the first one with default value. It does not call per each item for each mentioned field name, only one. Args: fields: Set of list argument field names. result_cls: Class to use result_cls() as initial result value to sum up. sum_func: A function to sum up result's chunks. single_item_call: Call a function with single item, not a new list of this single item. """ def inner(f: Callable): args_spec = inspect.getfullargspec(f) async def wrapped(*args, **kwargs) -> List: for field_name in fields: field_value = get_arg(args, kwargs, args_spec, field_name, from_default=False) if field_value: break else: raise ValueError(f"At least one argument of {fields} should be set.") return await request_per_item( field_name, result_cls=result_cls, sum_func=sum_func, single_item_call=single_item_call )(f)(*args, **kwargs) wrapped.__signature__ = inspect.signature(f) return wrapped return inner