1 """ISO 8601 date time string parsing
5 >>> iso8601.parse_date("2007-01-25T12:00:00Z")
6 datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>)
11 from datetime
import (
16 from decimal
import Decimal
21 __all__
= ["parse_date", "ParseError", "UTC"]
23 LOG
= logging
.getLogger(__name__
)
25 if sys
.version_info
>= (3, 0, 0):
28 _basestring
= basestring
31 # Adapted from http://delete.me.uk/2005/03/iso8601.html
32 ISO8601_REGEX
= re
.compile(
37 (-(?P<monthdash>[0-9]{1,2}))
40 (?!$) # Don't allow YYYYMM
44 (-(?P<daydash>[0-9]{1,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}
64 (?P<tz_minute>[0-9]{2}){0,1}
76 class ParseError(Exception):
77 """Raised when there is a problem parsing a date string"""
79 # Yoinked from python docs
85 def utcoffset(self
, dt
):
95 return "<iso8601.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
)
109 def __eq__(self
, other
):
110 if isinstance(other
, FixedOffset
):
112 (other
.__offset
== self
.__offset
)
114 (other
.__name
== self
.__name
)
116 if isinstance(other
, tzinfo
):
120 def __getinitargs__(self
):
121 return (self
.__offset
_hours
, self
.__offset
_minutes
, self
.__name
)
123 def utcoffset(self
, dt
):
126 def tzname(self
, dt
):
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
:
148 raise ParseError("Unable to read %s from %s" % (key
, d
))
152 def parse_timezone(matches
, default_timezone
=UTC
):
153 """Parses ISO 8601 time zone specs into tzinfo offsets
157 if matches
["timezone"] == "Z":
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).
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
)
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
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
)
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"))
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"],
213 except Exception as e
: