datetimeex.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import datetime
  2. import dateutil.parser
  3. import dateutil.tz.tz
  4. import re
  5. try:
  6. import yaml
  7. except ImportError:
  8. yaml = None
  9. def construct_yaml_timestamp(loader, node):
  10. loaded_dt = loader.construct_yaml_timestamp(node)
  11. if type(loaded_dt) is datetime.datetime and loaded_dt.tzinfo is None:
  12. timezone_match = re.search(
  13. ur'(Z|(?P<sign>[\+-])(?P<h>\d{2}):(?P<m>\d{2}))$',
  14. loader.construct_yaml_str(node),
  15. )
  16. if timezone_match:
  17. # loader.construct_yaml_timestamp converts to UTC
  18. loaded_dt = loaded_dt.replace(tzinfo = dateutil.tz.tz.tzutc())
  19. timezone_attr = timezone_match.groupdict()
  20. if timezone_attr['h']:
  21. timezone = dateutil.tz.tz.tzoffset(
  22. name = timezone_match.group(0),
  23. offset = (int(timezone_attr['h']) * 60 + int(timezone_attr['m'])) * 60
  24. * (-1 if timezone_attr['sign'] == '-' else 1),
  25. )
  26. loaded_dt = loaded_dt.astimezone(timezone)
  27. return loaded_dt
  28. def register_yaml_timestamp_constructor(loader, tag = u'tag:yaml.org,2002:timestamp'):
  29. loader.add_constructor(tag, construct_yaml_timestamp)
  30. class Duration(object):
  31. yaml_tag = u'!duration'
  32. def __init__(self, years = 0):
  33. self.years = years
  34. @property
  35. def years(self):
  36. return self._years
  37. @years.setter
  38. def years(self, years):
  39. if not type(years) is int:
  40. raise TypeError('expected int, %r given' % years)
  41. elif years < 0:
  42. raise ValueError('number of years must be >= 0, %r given' % years)
  43. else:
  44. self._years = years
  45. @property
  46. def isoformat(self):
  47. return 'P%dY' % self.years
  48. def __eq__(self, other):
  49. return (type(self) == type(other)
  50. and self.years == other.years)
  51. @classmethod
  52. def from_yaml(cls, loader, node):
  53. return cls(**loader.construct_mapping(node))
  54. @classmethod
  55. def register_yaml_constructor(cls, loader, tag = yaml_tag):
  56. loader.add_constructor(tag, cls.from_yaml)
  57. @classmethod
  58. def to_yaml(cls, dumper, duration, tag = yaml_tag):
  59. return dumper.represent_mapping(
  60. tag = tag,
  61. mapping = {k: v for k, v in {
  62. 'years': duration.years,
  63. }.items() if v != 0},
  64. )
  65. @classmethod
  66. def register_yaml_representer(cls, dumper):
  67. dumper.add_representer(cls, cls.to_yaml)
  68. class Period(object):
  69. yaml_tag = u'!period'
  70. _timestamp_iso_format = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d+)?(Z|[-\+]\d{2}:\d{2})?'
  71. _timeperiod_iso_format = r'(?P<start>%(t)s)\/(?P<end>%(t)s)' % {'t': _timestamp_iso_format}
  72. def __init__(self, start = None, end = None, isoformat = None):
  73. self._start = None
  74. self._end = None
  75. if (start or end) and isoformat:
  76. raise AttributeError('when providing isoformat no other parameters may be specified')
  77. elif isoformat:
  78. self.isoformat = isoformat
  79. else:
  80. self.start = start
  81. self.end = end
  82. @property
  83. def start(self):
  84. return self._start
  85. @start.setter
  86. def start(self, start):
  87. if not (start is None or type(start) is datetime.datetime):
  88. raise TypeError()
  89. self._start = start
  90. @property
  91. def end(self):
  92. return self._end
  93. @end.setter
  94. def end(self, end):
  95. if not (end is None or type(end) is datetime.datetime):
  96. raise TypeError()
  97. self._end = end
  98. @property
  99. def isoformat(self):
  100. if self.start is None or self.end is None:
  101. raise ValueError('both start and end must be set')
  102. return '%s/%s' % (
  103. self.start.isoformat().replace('+00:00', 'Z'),
  104. self.end.isoformat().replace('+00:00', 'Z'),
  105. )
  106. @isoformat.setter
  107. def isoformat(self, text):
  108. match = re.search('^%s$' % self.__class__._timeperiod_iso_format, text)
  109. if not match:
  110. raise ValueError(
  111. "given string '%s' does not match the supported pattern '%s'"
  112. % (text, self.__class__._timeperiod_iso_format)
  113. )
  114. attr = match.groupdict()
  115. self.start = dateutil.parser.parse(attr['start'])
  116. self.end = dateutil.parser.parse(attr['end'])
  117. def __eq__(self, other):
  118. return (type(self) == type(other)
  119. and self.start == other.start
  120. and self.end == other.end)
  121. @classmethod
  122. def from_yaml(cls, loader, node):
  123. return cls(**loader.construct_mapping(node))
  124. @classmethod
  125. def to_yaml(cls, dumper, period):
  126. return dumper.represent_mapping(
  127. tag = cls.yaml_tag,
  128. mapping = {
  129. 'start': period.start,
  130. 'end': period.end,
  131. },
  132. # represent datetime objects with !timestamp tag
  133. flow_style = False,
  134. )
  135. def __repr__(self):
  136. return '%s(%s)' % (self.__class__.__name__, ', '.join([
  137. 'start = %r' % self.start,
  138. 'end = %r' % self.end,
  139. ]))
  140. @classmethod
  141. def register_yaml_constructor(cls, loader, tag = yaml_tag):
  142. register_yaml_timestamp_constructor(loader)
  143. loader.add_constructor(tag, cls.from_yaml)
  144. @classmethod
  145. def register_yaml_representer(cls, dumper):
  146. dumper.add_representer(cls, cls.to_yaml)