Exemplo n.º 1
0
def add_offset(cycle_point, offset):
    """Add a (positive or negative) offset to a cycle point.

    Return the result.

    """
    my_parser = TimePointParser()
    my_target_point = my_parser.parse(cycle_point, dump_as_parsed=True)
    my_offset_parser = DurationParser()

    oper = "+"
    if offset.startswith(("-", "+")):
        oper = offset[0]
        offset = offset[1:]

    if not offset.startswith("P"):
        # TODO - raise appropriate exception
        raise ValueError("ERROR, bad offset format: %s" % offset)

    my_shift = my_offset_parser.parse(offset)
    if oper == "-":
        my_target_point -= my_shift
    else:
        my_target_point += my_shift

    return my_target_point
Exemplo n.º 2
0
    def __init__(self, parse_format=None, utc_mode=False, calendar_mode=None,
                 ref_point_str=None):
        """Constructor.

        parse_format -- If specified, parse with the specified format.
                        Otherwise, parse with one of the format strings in
                        self.PARSE_FORMATS. The format should be a string
                        compatible to strptime(3).

        utc_mode -- If True, parse/print in UTC mode rather than local or
                    other timezones.

        calendar_mode -- Set calendar mode for
                         metomi.isodatetime.data.Calendar.

        ref_point_str -- Set the reference time point for operations.
                         If not specified, operations use current date time.

        """
        self.parse_formats = self.PARSE_FORMATS
        self.custom_parse_format = parse_format
        self.utc_mode = utc_mode
        if self.utc_mode:
            assumed_time_zone = (0, 0)
        else:
            assumed_time_zone = None

        self.set_calendar_mode(calendar_mode)

        self.time_point_dumper = TimePointDumper()
        self.time_point_parser = TimePointParser(
            assumed_time_zone=assumed_time_zone)
        self.duration_parser = DurationParser()

        self.ref_point_str = ref_point_str
Exemplo n.º 3
0
 def coerce_interval(self, value, keys):
     """Coerce an ISO 8601 interval (or number: back-comp) into seconds."""
     value = self.strip_and_unquote(keys, value)
     if not value:
         # Allow explicit empty values.
         return None
     try:
         interval = DurationParser().parse(value)
     except ValueError:
         raise IllegalValueError("ISO 8601 interval", keys, value)
     days, seconds = interval.get_days_and_seconds()
     return DurationFloat(
         days * Calendar.default().SECONDS_IN_DAY + seconds)
Exemplo n.º 4
0
def duration_as(iso8601_duration, units):
    """Format an iso8601 duration string as the specified units.

    Args:
        iso8601_duration (str): Any valid ISO8601 duration as a string.
        units (str): Destination unit for the duration conversion

    Return:
        The total number of the specified unit contained in the specified
        duration as a floating-point number.

    Raises:
        ISO8601SyntaxError: In the event of an invalid datetime string.

    Examples:
        >>> # Basic usage.
        >>> duration_as('PT1M', 's')
        60.0
        >>> duration_as('PT1H', 'seconds')
        3600.0

        >>> # Exceptions.
        >>> duration_as('invalid', 's')  # doctest: +NORMALIZE_WHITESPACE
        Traceback (most recent call last):
        metomi.isodatetime.parsers.ISO8601SyntaxError: Invalid ISO 8601\
        duration representation: invalid
    """
    for converter_names in CONVERSIONS:
        if units.lower() in converter_names:
            converter = CONVERSIONS[converter_names]
            break
    else:
        raise ValueError('No matching units found for %s' % units)
    return converter(DurationParser().parse(iso8601_duration).get_seconds())
Exemplo n.º 5
0
    def coerce_interval(cls, value, keys):
        """Coerce an ISO 8601 interval into seconds.

        Examples:
            >>> CylcConfigValidator.coerce_interval('PT1H', None)
            3600.0

        """
        value = cls.strip_and_unquote(keys, value)
        if not value:
            # Allow explicit empty values.
            return None
        try:
            interval = DurationParser().parse(value)
        except ValueError:
            raise IllegalValueError("ISO 8601 interval", keys, value)
        days, seconds = interval.get_days_and_seconds()
        return DurationFloat(
            days * Calendar.default().SECONDS_IN_DAY + seconds)
Exemplo n.º 6
0
    def coerce_cycle_point(cls, value, keys):
        """Coerce value to a cycle point.

        Examples:
            >>> CylcConfigValidator.coerce_cycle_point('2000', None)
            '2000'
            >>> CylcConfigValidator.coerce_cycle_point('now', None)
            'now'
            >>> CylcConfigValidator.coerce_cycle_point('next(T-00)', None)
            'next(T-00)'

        """
        if not value:
            return None
        value = cls.strip_and_unquote(keys, value)
        if value == 'now':
            # Handle this later in config.py when workflow UTC mode is known.
            return value
        if "next" in value or "previous" in value:
            # Handle this later, as for "now".
            return value
        if value.isdigit():
            # Could be an old date-time cycle point format, or integer format.
            return value
        if "P" not in value and (
                value.startswith('-') or value.startswith('+')):
            # We don't know the value given for num expanded year digits...
            for i in range(1, 101):
                try:
                    TimePointParser(num_expanded_year_digits=i).parse(value)
                except IsodatetimeError:
                    continue
                return value
            raise IllegalValueError('cycle point', keys, value)
        if "P" in value:
            # ICP is an offset
            parser = DurationParser()
            try:
                if value.startswith("-"):
                    # parser doesn't allow negative duration with this setup?
                    parser.parse(value[1:])
                else:
                    parser.parse(value)
                return value
            except IsodatetimeError as exc:
                raise IllegalValueError('cycle point', keys, value, exc=exc)
        try:
            TimePointParser().parse(value)
        except IsodatetimeError as exc:
            if isinstance(exc, ISO8601SyntaxError):
                # Don't know cycling mode yet, so override ISO8601-specific msg
                details = {'msg': "Invalid cycle point"}
            else:
                details = {'exc': exc}
            raise IllegalValueError('cycle point', keys, value, **details)
        return value
    def __init__(self, offset_text):
        """Parse offset_text into a Duration-convertible form.

        Expect offset_text in this format:
        * A __ double underscore denotes an offset to the future.
          Otherwise, it is an offset to the past.
        * For the rest, use an ISO 8601 compatible duration.

        """
        BaseCycleOffset.__init__(self)
        if offset_text.startswith("__"):
            self.sign_factor = 1
        else:
            self.sign_factor = -1
        self.duration = DurationParser().parse(offset_text)
        self.duration *= self.sign_factor
Exemplo n.º 8
0
    def coerce_cycle_point(cls, value, keys):
        """Coerce value to a cycle point.

        Examples:
            >>> CylcConfigValidator.coerce_cycle_point('2000', None)
            '2000'
            >>> CylcConfigValidator.coerce_cycle_point('now', None)
            'now'
            >>> CylcConfigValidator.coerce_cycle_point('next(T-00)', None)
            'next(T-00)'

        """
        if not value:
            return None
        value = cls.strip_and_unquote(keys, value)
        if value == 'now':
            # Handle this later in config.py when the suite UTC mode is known.
            return value
        if "next" in value or "previous" in value:
            # Handle this later, as for "now".
            return value
        if value.isdigit():
            # Could be an old date-time cycle point format, or integer format.
            return value
        if "P" not in value and (
                value.startswith('-') or value.startswith('+')):
            # We don't know the value given for num expanded year digits...
            for i in range(1, 101):
                try:
                    TimePointParser(num_expanded_year_digits=i).parse(value)
                except ValueError:
                    continue
                return value
            raise IllegalValueError('cycle point', keys, value)
        if "P" in value:
            # ICP is an offset
            parser = DurationParser()
            try:
                if value.startswith("-"):
                    # parser doesn't allow negative duration with this setup?
                    parser.parse(value[1:])
                else:
                    parser.parse(value)
                return value
            except ValueError:
                raise IllegalValueError("cycle point", keys, value)
        try:
            TimePointParser().parse(value)
        except ValueError:
            raise IllegalValueError('cycle point', keys, value)
        return value
Exemplo n.º 9
0
def duration_as(iso8601_duration, units):
    """Format an :term:`ISO8601 duration` string as the specified units.

    Units for the conversion can be specified in a case-insensitive short or
    long form:

    - Seconds - "s" or "seconds"
    - Minutes - "m" or "minutes"
    - Hours - "h" or "hours"
    - Days - "d" or "days"
    - Weeks - "w" or "weeks"

    While the filtered value is a floating-point number, it is often required
    to supply an integer to workflow entities (e.g. environment variables) that
    require it.  This is accomplished by chaining filters:

    - ``{{CYCLE_INTERVAL | duration_as('h') | int}}`` - 24
    - ``{{CYCLE_SUBINTERVAL | duration_as('h') | int}}`` - 0
    - ``{{CYCLE_INTERVAL | duration_as('s') | int}}`` - 86400
    - ``{{CYCLE_SUBINTERVAL | duration_as('s') | int}}`` - 1800

    Args:
        iso8601_duration (str): Any valid ISO8601 duration as a string.
        units (str): Destination unit for the duration conversion

    Return:
        The total number of the specified unit contained in the specified
        duration as a floating-point number.

    Raises:
        ISO8601SyntaxError: In the event of an invalid datetime string.

    Python Examples:
        >>> # Basic usage.
        >>> duration_as('PT1M', 's')
        60.0
        >>> duration_as('PT1H', 'seconds')
        3600.0

        >>> # Exceptions.
        >>> duration_as('invalid value', 's')  # doctest: +NORMALIZE_WHITESPACE
        Traceback (most recent call last):
        metomi.isodatetime.exceptions.ISO8601SyntaxError: Invalid ISO 8601\
        duration representation: invalid value
        >>> duration_as('invalid unit', '#')  # doctest: +NORMALIZE_WHITESPACE
        Traceback (most recent call last):
        ValueError: No matching units found for #

    Jinja2 Examples:
       .. code-block:: cylc

          {% set CYCLE_INTERVAL = 'PT1D' %}
          {{ CYCLE_INTERVAL | duration_as('h') }}  # 24.0
          {% set CYCLE_SUBINTERVAL = 'PT30M' %}
          {{ CYCLE_SUBINTERVAL | duration_as('hours') }}  # 0.5
          {% set CYCLE_INTERVAL = 'PT1D' %}
          {{ CYCLE_INTERVAL | duration_as('s') }}  # 86400.0
          {% set CYCLE_SUBINTERVAL = 'PT30M' %}
          {{ CYCLE_SUBINTERVAL | duration_as('seconds') }}  # 1800.0

    """
    for converter_names in CONVERSIONS:
        if units.lower() in converter_names:
            converter = CONVERSIONS[converter_names]
            break
    else:
        raise ValueError('No matching units found for %s' % units)
    return converter(DurationParser().parse(iso8601_duration).get_seconds())
Exemplo n.º 10
0
class RoseDateTimeOperator:
    """A class to parse and print date string with an offset."""

    CURRENT_TIME_DUMP_FORMAT = "CCYY-MM-DDThh:mm:ss+hh:mm"
    CURRENT_TIME_DUMP_FORMAT_Z = "CCYY-MM-DDThh:mm:ssZ"

    NEGATIVE = "-"

    # strptime formats and their compatibility with the ISO 8601 parser.
    PARSE_FORMATS = [
        ("%a %b %d %H:%M:%S %Y", True),  # ctime
        ("%a %b %d %H:%M:%S %Z %Y", True),  # Unix "date"
        ("%Y-%m-%dT%H:%M:%S", False),  # ISO8601, extended
        ("%Y%m%dT%H%M%S", False),  # ISO8601, basic
        ("%Y%m%d%H", False),  # Cylc (current)
    ]

    REC_OFFSET = re.compile(r"""\A[\+\-]?(?:\d+[wdhms])+\Z""", re.I)

    REC_OFFSET_FIND = re.compile(r"""(?P<num>\d+)(?P<unit>[wdhms])""")

    STR_NOW = "now"
    STR_REF = "ref"

    TASK_CYCLE_TIME_ENV = "ROSE_TASK_CYCLE_TIME"

    UNITS = {
        "w": "weeks",
        "d": "days",
        "h": "hours",
        "m": "minutes",
        "s": "seconds",
    }

    def __init__(
        self,
        parse_format=None,
        utc_mode=False,
        calendar_mode=None,
        ref_point_str=None,
    ):
        """Constructor.

        parse_format -- If specified, parse with the specified format.
                        Otherwise, parse with one of the format strings in
                        self.PARSE_FORMATS. The format should be a string
                        compatible to strptime(3).

        utc_mode -- If True, parse/print in UTC mode rather than local or
                    other timezones.

        calendar_mode -- Set calendar mode for
                         metomi.isodatetime.data.Calendar.

        ref_point_str -- Set the reference time point for operations.
                         If not specified, operations use current date time.

        """
        self.parse_formats = self.PARSE_FORMATS
        self.custom_parse_format = parse_format
        self.utc_mode = utc_mode
        if self.utc_mode:
            assumed_time_zone = (0, 0)
        else:
            assumed_time_zone = None

        self.set_calendar_mode(calendar_mode)

        self.time_point_dumper = TimePointDumper()
        self.time_point_parser = TimePointParser(
            assumed_time_zone=assumed_time_zone)
        self.duration_parser = DurationParser()

        self.ref_point_str = ref_point_str

    def date_format(self, print_format, time_point=None):
        """Reformat time_point according to print_format.

        time_point -- The time point to format.
                      Otherwise, use ref date time.

        """
        if time_point is None:
            time_point = self.date_parse()[0]
        if print_format is None:
            return str(time_point)
        if "%" in print_format:
            try:
                return time_point.strftime(print_format)
            except ValueError:
                return self.get_datetime_strftime(time_point, print_format)
        return self.time_point_dumper.dump(time_point, print_format)

    def date_parse(self, time_point_str=None):
        """Parse time_point_str.

        Return (t, format) where t is a metomi.isodatetime.data.TimePoint
        object and format is the format that matches time_point_str.

        time_point_str -- The time point string to parse.
                          Otherwise, use ref time.

        """
        if time_point_str is None or time_point_str == self.STR_REF:
            time_point_str = self.ref_point_str
        if time_point_str is None or time_point_str == self.STR_NOW:
            time_point = get_timepoint_for_now()
            if self.utc_mode or time_point.get_time_zone_utc():  # is in UTC
                parse_format = self.CURRENT_TIME_DUMP_FORMAT_Z
            else:
                parse_format = self.CURRENT_TIME_DUMP_FORMAT
        elif self.custom_parse_format is not None:
            parse_format = self.custom_parse_format
            time_point = self.strptime(time_point_str, parse_format)
        else:
            parse_formats = list(self.parse_formats)
            time_point = None
            while parse_formats:
                parse_format, should_use_datetime = parse_formats.pop(0)
                try:
                    if should_use_datetime:
                        time_point = self.get_datetime_strptime(
                            time_point_str, parse_format)
                    else:
                        time_point = self.time_point_parser.strptime(
                            time_point_str, parse_format)
                    break
                except ValueError:
                    pass
            if time_point is None:
                time_point = self.time_point_parser.parse(time_point_str,
                                                          dump_as_parsed=True)
                parse_format = time_point.dump_format
        if self.utc_mode:
            time_point = time_point.to_utc()
        return time_point, parse_format

    def date_shift(self, time_point=None, offset=None):
        """Return a date string with an offset.

        time_point -- A time point or time point string.
                      Otherwise, use current time.

        offset -- If specified, it should be a string containing the offset
                  that has the format "[+/-]nU[nU...]" where "n" is an
                  integer, and U is a unit matching a key in self.UNITS.

        """
        if time_point is None:
            time_point = self.date_parse()[0]
        # Offset
        if offset:
            sign = "+"
            if offset.startswith("-") or offset.startswith("+"):
                sign = offset[0]
                offset = offset[1:]
            if offset.startswith("P"):
                # Parse and apply.
                try:
                    duration = self.duration_parser.parse(offset)
                except ValueError:
                    raise OffsetValueError(offset)
                if sign == "-":
                    time_point -= duration
                else:
                    time_point += duration
            else:
                # Backwards compatibility for e.g. "-1h"
                if not self.is_offset(offset):
                    raise OffsetValueError(offset)
                for num, unit in self.REC_OFFSET_FIND.findall(offset.lower()):
                    num = int(num)
                    if sign == "-":
                        num = -num
                    key = self.UNITS[unit]
                    time_point += Duration(**{key: num})

        return time_point

    def date_diff(self, time_point_1=None, time_point_2=None):
        """Return (duration, is_negative) between two TimePoint objects.

        duration -- is a Duration instance.
        is_negative -- is a RoseDateTimeOperator.NEGATIVE if time_point_2 is
                       in the past of time_point_1.
        """
        if time_point_2 < time_point_1:
            return (time_point_1 - time_point_2, self.NEGATIVE)
        else:
            return (time_point_2 - time_point_1, "")

    @classmethod
    def date_diff_format(cls, print_format, duration, sign):
        """Format a duration."""
        if print_format:
            delta_lookup = {
                "y": duration.years,
                "m": duration.months,
                "d": duration.days,
                "h": duration.hours,
                "M": duration.minutes,
                "s": duration.seconds,
            }
            expression = ""
            for item in print_format:
                if item in delta_lookup:
                    if float(delta_lookup[item]).is_integer():
                        expression += str(int(delta_lookup[item]))
                    else:
                        expression += str(delta_lookup[item])
                else:
                    expression += item
            return sign + expression
        else:
            return sign + str(duration)

    @staticmethod
    def get_calendar_mode():
        """Get current calendar mode."""
        return Calendar.default().mode

    def is_offset(self, offset):
        """Return True if the string offset can be parsed as an offset."""
        return self.REC_OFFSET.match(offset) is not None

    @staticmethod
    def set_calendar_mode(calendar_mode=None):
        """Set calendar mode for subsequent operations.

        Raise KeyError if calendar_mode is invalid.

        """
        if not calendar_mode:
            calendar_mode = os.getenv("ROSE_CYCLING_MODE")

        if calendar_mode and calendar_mode in Calendar.MODES:
            Calendar.default().set_mode(calendar_mode)

    def strftime(self, time_point, print_format):
        """Use either the metomi.isodatetime or datetime strftime time
        formatting.
        """
        try:
            return time_point.strftime(print_format)
        except ValueError:
            return self.get_datetime_strftime(time_point, print_format)

    def strptime(self, time_point_str, parse_format):
        """Use either the isodatetime or datetime strptime time parsing."""
        try:
            return self.time_point_parser.strptime(time_point_str,
                                                   parse_format)
        except ValueError:
            return self.get_datetime_strptime(time_point_str, parse_format)

    @classmethod
    def get_datetime_strftime(cls, time_point, print_format):
        """Use the datetime library's strftime as a fallback."""
        year, month, day = time_point.get_calendar_date()
        hour, minute, second = time_point.get_hour_minute_second()
        microsecond = int(1.0e6 * (second - int(second)))
        hour = int(hour)
        minute = int(minute)
        second = int(second)
        date_time = datetime(year, month, day, hour, minute, second,
                             microsecond)
        return date_time.strftime(print_format)

    def get_datetime_strptime(self, time_point_str, parse_format):
        """Use the datetime library's strptime as a fallback."""
        date_time = datetime.strptime(time_point_str, parse_format)
        return self.time_point_parser.parse(date_time.isoformat())