param: Use IDL-based constants for NBT and NBT dgram ports
[Samba.git] / third_party / pyiso8601 / iso8601 / iso8601.py
blob1ae810ab9ae297f77f19a4912ec86d3502badfbc
1 """ISO 8601 date time string parsing
3 Basic usage:
4 >>> import iso8601
5 >>> iso8601.parse_date("2007-01-25T12:00:00Z")
6 datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>)
7 >>>
9 """
11 from datetime import (
12 datetime,
13 timedelta,
14 tzinfo
16 from decimal import Decimal
17 import logging
18 import sys
19 import re
21 __all__ = ["parse_date", "ParseError", "UTC"]
23 LOG = logging.getLogger(__name__)
25 if sys.version_info >= (3, 0, 0):
26 _basestring = str
27 else:
28 _basestring = basestring
31 # Adapted from http://delete.me.uk/2005/03/iso8601.html
32 ISO8601_REGEX = re.compile(
33 r"""
34 (?P<year>[0-9]{4})
37 (-(?P<monthdash>[0-9]{1,2}))
39 (?P<month>[0-9]{2})
40 (?!$) # Don't allow YYYYMM
44 (-(?P<daydash>[0-9]{1,2}))
46 (?P<day>[0-9]{2})
50 (?P<separator>[ T])
51 (?P<hour>[0-9]{2})
52 (:{0,1}(?P<minute>[0-9]{2})){0,1}
54 :{0,1}(?P<second>[0-9]{1,2})
55 (\.(?P<second_fraction>[0-9]+)){0,1}
56 ){0,1}
57 (?P<timezone>
61 (?P<tz_sign>[-+])
62 (?P<tz_hour>[0-9]{2})
63 :{0,1}
64 (?P<tz_minute>[0-9]{2}){0,1}
66 ){0,1}
67 ){0,1}
69 ){0,1} # YYYY-MM
70 ){0,1} # YYYY only
72 """,
73 re.VERBOSE
76 class ParseError(Exception):
77 """Raised when there is a problem parsing a date string"""
79 # Yoinked from python docs
80 ZERO = timedelta(0)
81 class Utc(tzinfo):
82 """UTC Timezone
84 """
85 def utcoffset(self, dt):
86 return ZERO
88 def tzname(self, dt):
89 return "UTC"
91 def dst(self, dt):
92 return ZERO
94 def __repr__(self):
95 return "<iso8601.Utc>"
97 UTC = Utc()
99 class FixedOffset(tzinfo):
100 """Fixed offset in hours and minutes from UTC
103 def __init__(self, offset_hours, offset_minutes, name):
104 self.__offset_hours = offset_hours # Keep for later __getinitargs__
105 self.__offset_minutes = offset_minutes # Keep for later __getinitargs__
106 self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes)
107 self.__name = name
109 def __eq__(self, other):
110 if isinstance(other, FixedOffset):
111 return (
112 (other.__offset == self.__offset)
114 (other.__name == self.__name)
116 if isinstance(other, tzinfo):
117 return other == self
118 return False
120 def __getinitargs__(self):
121 return (self.__offset_hours, self.__offset_minutes, self.__name)
123 def utcoffset(self, dt):
124 return self.__offset
126 def tzname(self, dt):
127 return self.__name
129 def dst(self, dt):
130 return ZERO
132 def __repr__(self):
133 return "<FixedOffset %r %r>" % (self.__name, self.__offset)
135 def to_int(d, key, default_to_zero=False, default=None, required=True):
136 """Pull a value from the dict and convert to int
138 :param default_to_zero: If the value is None or empty, treat it as zero
139 :param default: If the value is missing in the dict use this default
142 value = d.get(key) or default
143 LOG.debug("Got %r for %r with default %r", value, key, default)
144 if (value in ["", None]) and default_to_zero:
145 return 0
146 if value is None:
147 if required:
148 raise ParseError("Unable to read %s from %s" % (key, d))
149 else:
150 return int(value)
152 def parse_timezone(matches, default_timezone=UTC):
153 """Parses ISO 8601 time zone specs into tzinfo offsets
157 if matches["timezone"] == "Z":
158 return UTC
159 # This isn't strictly correct, but it's common to encounter dates without
160 # timezones so I'll assume the default (which defaults to UTC).
161 # Addresses issue 4.
162 if matches["timezone"] is None:
163 return default_timezone
164 sign = matches["tz_sign"]
165 hours = to_int(matches, "tz_hour")
166 minutes = to_int(matches, "tz_minute", default_to_zero=True)
167 description = "%s%02d:%02d" % (sign, hours, minutes)
168 if sign == "-":
169 hours = -hours
170 minutes = -minutes
171 return FixedOffset(hours, minutes, description)
173 def parse_date(datestring, default_timezone=UTC):
174 """Parses ISO 8601 dates into datetime objects
176 The timezone is parsed from the date string. However it is quite common to
177 have dates without a timezone (not strictly correct). In this case the
178 default timezone specified in default_timezone is used. This is UTC by
179 default.
181 :param datestring: The date to parse as a string
182 :param default_timezone: A datetime tzinfo instance to use when no timezone
183 is specified in the datestring. If this is set to
184 None then a naive datetime object is returned.
185 :returns: A datetime.datetime instance
186 :raises: ParseError when there is a problem parsing the date or
187 constructing the datetime instance.
190 if not isinstance(datestring, _basestring):
191 raise ParseError("Expecting a string %r" % datestring)
192 m = ISO8601_REGEX.match(datestring)
193 if not m:
194 raise ParseError("Unable to parse date string %r" % datestring)
195 groups = m.groupdict()
196 LOG.debug("Parsed %s into %s with default timezone %s", datestring, groups, default_timezone)
198 tz = parse_timezone(groups, default_timezone=default_timezone)
200 groups["second_fraction"] = int(Decimal("0.%s" % (groups["second_fraction"] or 0)) * Decimal("1000000.0"))
202 try:
203 return datetime(
204 year=to_int(groups, "year"),
205 month=to_int(groups, "month", default=to_int(groups, "monthdash", required=False, default=1)),
206 day=to_int(groups, "day", default=to_int(groups, "daydash", required=False, default=1)),
207 hour=to_int(groups, "hour", default_to_zero=True),
208 minute=to_int(groups, "minute", default_to_zero=True),
209 second=to_int(groups, "second", default_to_zero=True),
210 microsecond=groups["second_fraction"],
211 tzinfo=tz,
213 except Exception as e:
214 raise ParseError(e)