Source code for roocs_utils.parameter.param_utils

import calendar
from collections.abc import Sequence

from roocs_utils.exceptions import InvalidParameterValue
from roocs_utils.utils.file_utils import FileMapper
from roocs_utils.utils.time_utils import str_to_AnyCalendarDateTime


# Global variables that are generally useful
month_map = {name.lower(): num for num, name in enumerate(calendar.month_abbr) if num}

time_comp_limits = {
    "year": None,
    "month": (1, 12),
    "day": (1, 40),  # allowing for strange calendars
    "hour": (0, 23),
    "minute": (0, 59),
    "second": (0, 59),
}


# A set of simple parser functions
[docs]def parse_range(x, caller): if isinstance(x, Sequence) and len(x) == 1: x = x[0] if x in ("/", None, ""): start = None end = None elif isinstance(x, str): if "/" not in x: raise InvalidParameterValue( f"{caller} should be passed in as a range separated by /" ) # empty string either side of '/' is converted to None start, end = [i.strip() or None for i in x.split("/")] elif isinstance(x, Sequence): if len(x) != 2: raise InvalidParameterValue( f"{caller} should be a range. Expected 2 values, " f"received {len(x)}" ) start, end = x else: raise InvalidParameterValue(f"{caller} is not in an accepted format") return start, end
[docs]def parse_sequence(x, caller): if x in (None, ""): sequence = None # check str or bytes elif isinstance(x, (str, bytes)): sequence = [i.strip() for i in x.strip().split(",")] elif isinstance(x, FileMapper): sequence = [x] elif isinstance(x, Sequence): sequence = x else: raise InvalidParameterValue(f"{caller} is not in an accepted format") return sequence
[docs]def parse_datetime(dt, defaults=None): """Parses string to datetime and returns isoformat string for it. If `defaults` is set, use that in case `dt` is None.""" return str(str_to_AnyCalendarDateTime(dt, defaults=defaults))
[docs]class Series: """ A simple class for handling a series selection, created by any sequence as input. It has a `value` that holds the sequence as a list. """ def __init__(self, *data): if len(data) == 1: data = data[0] self.value = parse_sequence(data, caller=self.__class__.__name__)
[docs]class Interval: """ A simple class for handling an interval of any type. It holds a `start` and `end` but does not try to resolve the range, it is just a container to be used by other tools. The contents can be of any type, such as datetimes, strings etc. """ def __init__(self, *data): self.value = parse_range(data, self.__class__.__name__)
[docs]class TimeComponents: """ A simple class for parsing and representing a set of time components. The components are stored in a dictionary of {time_comp: values}, such as: {"year": [2000, 2001], "month": [1, 2, 3]} Note that you can provide month strings as strings or numbers, e.g.: "feb", "Feb", "February", 2 """ def __init__( self, year=None, month=None, day=None, hour=None, minute=None, second=None ): comps = ("year", "month", "day", "hour", "minute", "second") self.value = {} for comp in comps: if comp in locals(): value = locals()[comp] # Only add to dict if defined if value is not None: self.value[comp] = self._parse_component(comp, value) def _parse_component(self, time_comp, value): limits = time_comp_limits[time_comp] if isinstance(value, str): if "," in value: value = value.split(",") else: value = [value] if not isinstance(value, Sequence): value = [value] def _month_to_int(month): if isinstance(month, str): month = month_map.get(month.lower()[:3], month) return int(month) if time_comp == "month": value = [_month_to_int(month) for month in value] else: value = [int(i) for i in value] if limits: mn, mx = min(value), max(value) if mn < limits[0] or mx > limits[1]: raise ValueError( f"Some time components are out of range for {time_comp}: " f"({mn}, {mx})" ) return value
[docs]def string_to_dict(s, splitters=("|", ":", ",")): """Convert a string to a dictionary of dictionaries, based on splitting rules: splitters.""" dct = {} for tdict in s.strip().split(splitters[0]): key, value = tdict.split(splitters[1]) dct[key] = value.split(splitters[2]) return dct
[docs]def to_float(i, allow_none=True): try: if allow_none and i is None: return i return float(i) except Exception: raise InvalidParameterValue("Values must be valid numbers")
# Create some aliases for creating simple selection types series = time_series = level_series = area = collection = dimensions = Series interval = time_interval = level_interval = Interval time_components = TimeComponents