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
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 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)
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())
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)
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
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
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())
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())