from copy import deepcopy
from covsirphy.util.config import config
[docs]
def deprecate(old, new=None, version=None, ref=None):
    """
    Decorator to raise deprecation warning.
    Args:
        old (str): description of the old method/function
        new (str or None): description of the new method/function
        version (str or None): version number, like 2.7.3-alpha
        ref (str or None): reference URL of the new method/function
    """
    def _deprecate(func):
        def wrapper(*args, **kwargs):
            version_str = "." if version is None else f", version >= {version}."
            message = "" if ref is None else f" Refer to {ref}."
            if new is None:
                comment = f"{old} was deprecated{version_str}{message}"
            else:
                comment = f"Please use {new} rather than {old}{version_str}{message}"
            config.warning(message=comment, category=DeprecationWarning)
            return func(*args, **kwargs)
        return wrapper
    return _deprecate 
[docs]
def experimental(name, version):
    """
    Decorator to raise ExperimentalWarning because the method/function is experimental.
    Args:
        name (str): description of the method/function
        version (str): version number, like 2.7.3-alpha
    """
    def _experimental(func):
        def wrapper(*args, **kwargs):
            comment = f"{name} can be used from {version}, but this is experimental." \
                
"Its name and arguments may be changed later."
            config.warning(message=comment, category=ExperimentalWarning)
            return func(*args, **kwargs)
        return wrapper
    return _experimental 
class _BaseWarning(Warning):
    """Basic class of warning.
    """
    pass
[docs]
class ExperimentalWarning(Warning):
    """Class to explain the method/function is experimental and its name,
    features and arguments may changed later.
    """
    pass 
class _BaseException(Exception):
    """Basic class of exception.
    Args:
        message (str): main message of error, should be set in child classes
        details (str or None): details of error
        log (str): description used by logger
    """
    def __init__(self, message, details=None, log="exception raised"):
        config.error(log)
        self._message = str(message)
        self._details = "" if details is None else f" {details}."
    def __str__(self):
        return f"{self._message}. {self._details}"
class _ValidationError(_BaseException):
    """Basic class of exception raised when validation.
    Args:
        name (str): name of the target
        message (str): main message of error, should be set in child classes
        details (str or None): details of error
        log (str): description used by logger
    """
    def __init__(self, name, message, details=None):
        log = f"validation of {name} failed"
        super().__init__(message=message, details=details, log=log)
[docs]
class NotIncludedError(_ValidationError):
    """Error when a necessary key was not included in a container.
    Args:
        key_name (str): key name
        container_name (str): name of the container
        details (str or None): details of error
    """
    def __init__(self, key_name, container_name, details=None):
        message = f"'{key_name}' was not included in the '{container_name}'"
        super().__init__(name=key_name, message=message, details=details) 
[docs]
class NAFoundError(_ValidationError):
    """Error when NA values are included un-expectedly.
    Args:
        name (str): name of the target
        value (str or None): value of the target
        details (str or None): details of error
    """
    def __init__(self, name, value=None, details=None):
        message = f"'{name}' has NA(s) un-expectedly"
        if value is not None:
            message += f", '{value}'"
        super().__init__(name=name, message=message, details=details) 
[docs]
class NotEnoughDataError(_ValidationError):
    """Error when we do not have enough data for analysis.
    Args:
        name (str): name of the target
        value (str): value of the target
        required_n (int): required number of records
        details (str or None): details of error
    """
    def __init__(self, name, value, required_n, details=None):
        message = f"We need more than {required_n} records, but '{name}' has only {len(value)} records at this time"
        super().__init__(name=name, message=message, details=details) 
[docs]
class UnExpectedNoneError(_ValidationError):
    """Error when a value is None un-expectedly.
    Args:
        name (str): name of the target
        details (str or None): details of error
    """
    def __init__(self, name, details=None):
        message = f"'{name}' is None un-expectedly"
        super().__init__(name=name, message=message, details=details) 
[docs]
class NotNoneError(_ValidationError):
    """Error when a value must be None but not None un-expectedly.
    Args:
        name (str): name of the target
        value (str): value of the target
        details (str or None): details of error
    """
    def __init__(self, name, value, details=None):
        message = f"'{name}' must be None, but has value '{value}'"
        super().__init__(name=name, message=message, details=details) 
[docs]
class UnExecutedError(_BaseException):
    """
    Error when we have unexecuted methods that we need to run in advance.
    Args:
        name (str): method name to run in advance
        details (str or None): details of error
    """
    def __init__(self, name, details=None):
        message = f"Please execute {name} in advance"
        log = f"{name} not executed"
        super().__init__(message=message, details=details, log=log) 
[docs]
class NotRegisteredError(UnExecutedError):
    """
    Error when no records have been registered yet.
    """
    pass 
[docs]
class NotSubclassError(_ValidationError):
    """Error when an object is not a subclass of the parent class un-expectedly.
    Args:
        name (str): name of the target
        target (object): target object
        parent (object): expected parent class
        details (str or None): details of error
    """
    def __init__(self, name, target, parent, details=None):
        message = f"'{name}' must be a sub-class of {parent}, but {type(target)} was applied"
        super().__init__(name=name, message=message, details=details) 
[docs]
class UnExpectedTypeError(_ValidationError):
    """Error when an object cannot be converted to an instance un-expectedly.
    Args:
        name (str): name of the target
        target (object): target object
        expected (object): expected type
        details (str or None): details of error
    """
    def __init__(self, name, target, expected, details=None):
        message = f"We could not convert '{name}' to an instance of {expected} because that of {type(target)} was applied"
        super().__init__(name=name, message=message, details=details) 
[docs]
class EmptyError(_ValidationError):
    """Error when the dataframe is empty un-expectedly.
    Args:
        name (str): name of the target
        details (str or None): details of error
    """
    def __init__(self, name, details=None):
        message = f"'Empty dataframe/series was applied as {name}' un-expectedly"
        super().__init__(name=name, message=message, details=details) 
[docs]
class UnExpectedValueRangeError(_ValidationError):
    """Error when the value is out of value range.
    Args:
        name (str): name of the target
        target (object): target object
        value_range (tuple(int or None, int or None)): value range, None means un-specified
        details (str or None): details of error
    """
    def __init__(self, name, target, value_range, details=None):
        _min, _max = value_range
        if _min is None:
            s = "is not in the expected value range" if _max is None else f"must be under or equal to {_max}"
        else:
            s = f"must be over or equal to {_min}" if _max is None else f"is not in the expected value range ({_min}, {_max})"
        message = f"'{name}' {s}, but {target} was applied"
        super().__init__(name=name, message=message, details=details) 
[docs]
class UnExpectedValueError(_ValidationError):
    """
    Error when unexpected value was applied as the value of an argument.
    Args:
        name (str): argument name
        value (object): value user applied
        candidates (list[object]): candidates of the argument
        details (str or None): details of error
    """
    def __init__(self, name, value, candidates, details=None):
        c_str = ", ".join(candidates)
        message = f"'{name}' must be selected from [{c_str}], but {value} was applied"
        super().__init__(name=name, message=message, details=details) 
[docs]
class UnExpectedLengthError(_ValidationError):
    """
    Error when a sequence has un-expended length.
    Args:
        name (str): argument name
        value (object): value user applied
        length (int): length of the sequence
        details (str or None): details of error
    """
    def __init__(self, name, value, length, details=None):
        message = f"The length of '{name}' must be {length}, but {len(value)} was applied"
        super().__init__(name=name, message=message, details=details) 
[docs]
class SubsetNotFoundError(_BaseException):
    """Error when subset was failed with specified arguments.
    Args:
        geo (tuple(list[str] or tuple(str) or str) or str or None): location names to filter or None
        country (str): country name
        country_alias (str or None): country name used in the dataset
        province (str or None): province name
        start_date (str or None): start date, like 22Jan2020
        end_date (str or None): end date, like 01Feb2020
        date (str or None): specified date, like 22Jan2020
        details (str or None): details of error
    """
    def __init__(self, geo=None, country=None, country_alias=None, province=None,
                 start_date=None, end_date=None, date=None, details=None):
        self.area = self._area(geo, country, country_alias, province)
        self.date = self._date(start_date, end_date, date)
        message = f"No records in {self.area}{self.date} were found"
        log = "data subsetting failed"
        super().__init__(message=message, details=details, log=log)
    @staticmethod
    def _area(geo, country, country_alias, province):
        """
        Error when subset was failed with specified arguments.
        Args:
            geo (tuple(list[str] or tuple(str) or str) or str or None): location names to filter or None
            country (str): country name
            country_alias (str or None): country name used in the dataset
            province (str or None): province name
        Returns:
            str: area name
        """
        if geo is None and country is None:
            return "the world"
        if geo is not None:
            geo_converted = deepcopy(geo)
        elif province is None:
            geo_converted = (country if country_alias is None else f"{country} ({country_alias})",)
        else:
            geo_converted = (country if country_alias is None else f"{country} ({country_alias})", province)
        names = [
            info if isinstance(info, str) else "-" if info is None else "_".join(list(info))
            for info in ([geo_converted] if isinstance(geo_converted, str) else geo_converted)]
        return "/".join(names[::-1])
    @staticmethod
    def _date(start_date, end_date, date):
        """
        Error when subset was failed with specified arguments.
        Args:
            start_date (str or None): start date, like 22Jan2020
            end_date (str or None): end date, like 01Feb2020
            date (str or None): specified date, like 22Jan2020
        """
        if date is not None:
            return f" on {date}"
        start_str = "" if start_date is None else f" from {start_date}"
        end_str = "" if end_date is None else f" to {end_date}"
        return f"{start_str}{end_str}" 
[docs]
class ScenarioNotFoundError(_BaseException):
    """Error when unregistered scenario name was specified.
    Args:
        name (str): scenario name
        details (str or None): details of error
    """
    def __init__(self, name, details=None):
        message = f"{name} scenario is not registered"
        log = "scenario selection failed"
        super().__init__(message=message, details=details, log=log)