first version of upstream package from subversion master upstream-unstable
authorBenjamin Mako Hill <mako@atdot.cc>
Fri, 11 Feb 2011 18:13:14 +0000 (13:13 -0500)
committerBenjamin Mako Hill <mako@atdot.cc>
Fri, 11 Feb 2011 18:13:14 +0000 (13:13 -0500)
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README [new file with mode: 0644]
iso8601/__init__.py [new file with mode: 0644]
iso8601/iso8601.py [new file with mode: 0644]
iso8601/test_iso8601.py [new file with mode: 0644]
setup.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..1b53ace
--- /dev/null
@@ -0,0 +1 @@
+.svn/
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..5ca93da
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2007 Michael Twomey
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..5ec9d45
--- /dev/null
+++ b/README
@@ -0,0 +1,26 @@
+A simple package to deal with ISO 8601 date time formats.
+
+ISO 8601 defines a neutral, unambiguous date string format, which also
+has the property of sorting naturally.
+
+e.g. YYYY-MM-DDTHH:MM:SSZ or 2007-01-25T12:00:00Z
+
+Currently this covers only the most common date formats encountered, not
+all of ISO 8601 is handled.
+
+Currently the following formats are handled:
+
+* 2006-01-01T00:00:00Z
+* 2006-01-01T00:00:00[+-]00:00
+
+I'll add more as I encounter them in my day to day life. Patches with 
+new formats and tests will be gratefully accepted of course :)
+
+References:
+
+* http://www.cl.cam.ac.uk/~mgk25/iso-time.html - simple overview
+
+* http://hydracen.com/dx/iso8601.htm - more detailed enumeration of
+  valid formats.
+
+See the LICENSE file for the license this package is released under.
diff --git a/iso8601/__init__.py b/iso8601/__init__.py
new file mode 100644 (file)
index 0000000..e72e356
--- /dev/null
@@ -0,0 +1 @@
+from iso8601 import *
diff --git a/iso8601/iso8601.py b/iso8601/iso8601.py
new file mode 100644 (file)
index 0000000..f923938
--- /dev/null
@@ -0,0 +1,102 @@
+"""ISO 8601 date time string parsing
+
+Basic usage:
+>>> import iso8601
+>>> iso8601.parse_date("2007-01-25T12:00:00Z")
+datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>)
+>>>
+
+"""
+
+from datetime import datetime, timedelta, tzinfo
+import re
+
+__all__ = ["parse_date", "ParseError"]
+
+# Adapted from http://delete.me.uk/2005/03/iso8601.html
+ISO8601_REGEX = re.compile(r"(?P<year>[0-9]{4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})"
+    r"((?P<separator>.)(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2})(:(?P<second>[0-9]{2})(\.(?P<fraction>[0-9]+))?)?"
+    r"(?P<timezone>Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?"
+)
+TIMEZONE_REGEX = re.compile("(?P<prefix>[+-])(?P<hours>[0-9]{2}).(?P<minutes>[0-9]{2})")
+
+class ParseError(Exception):
+    """Raised when there is a problem parsing a date string"""
+
+# Yoinked from python docs
+ZERO = timedelta(0)
+class Utc(tzinfo):
+    """UTC
+    
+    """
+    def utcoffset(self, dt):
+        return ZERO
+
+    def tzname(self, dt):
+        return "UTC"
+
+    def dst(self, dt):
+        return ZERO
+UTC = Utc()
+
+class FixedOffset(tzinfo):
+    """Fixed offset in hours and minutes from UTC
+    
+    """
+    def __init__(self, offset_hours, offset_minutes, name):
+        self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes)
+        self.__name = name
+
+    def utcoffset(self, dt):
+        return self.__offset
+
+    def tzname(self, dt):
+        return self.__name
+
+    def dst(self, dt):
+        return ZERO
+    
+    def __repr__(self):
+        return "<FixedOffset %r>" % self.__name
+
+def parse_timezone(tzstring, default_timezone=UTC):
+    """Parses ISO 8601 time zone specs into tzinfo offsets
+    
+    """
+    if tzstring == "Z":
+        return default_timezone
+    # This isn't strictly correct, but it's common to encounter dates without
+    # timezones so I'll assume the default (which defaults to UTC).
+    # Addresses issue 4.
+    if tzstring is None:
+        return default_timezone
+    m = TIMEZONE_REGEX.match(tzstring)
+    prefix, hours, minutes = m.groups()
+    hours, minutes = int(hours), int(minutes)
+    if prefix == "-":
+        hours = -hours
+        minutes = -minutes
+    return FixedOffset(hours, minutes, tzstring)
+
+def parse_date(datestring, default_timezone=UTC):
+    """Parses ISO 8601 dates into datetime objects
+    
+    The timezone is parsed from the date string. However it is quite common to
+    have dates without a timezone (not strictly correct). In this case the
+    default timezone specified in default_timezone is used. This is UTC by
+    default.
+    """
+    if not isinstance(datestring, basestring):
+        raise ParseError("Expecting a string %r" % datestring)
+    m = ISO8601_REGEX.match(datestring)
+    if not m:
+        raise ParseError("Unable to parse date string %r" % datestring)
+    groups = m.groupdict()
+    tz = parse_timezone(groups["timezone"], default_timezone=default_timezone)
+    if groups["fraction"] is None:
+        groups["fraction"] = 0
+    else:
+        groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6)
+    return datetime(int(groups["year"]), int(groups["month"]), int(groups["day"]),
+        int(groups["hour"]), int(groups["minute"]), int(groups["second"]),
+        int(groups["fraction"]), tz)
diff --git a/iso8601/test_iso8601.py b/iso8601/test_iso8601.py
new file mode 100644 (file)
index 0000000..ff9e273
--- /dev/null
@@ -0,0 +1,111 @@
+import iso8601
+
+def test_iso8601_regex():
+    assert iso8601.ISO8601_REGEX.match("2006-10-11T00:14:33Z")
+
+def test_timezone_regex():
+    assert iso8601.TIMEZONE_REGEX.match("+01:00")
+    assert iso8601.TIMEZONE_REGEX.match("+00:00")
+    assert iso8601.TIMEZONE_REGEX.match("+01:20")
+    assert iso8601.TIMEZONE_REGEX.match("-01:00")
+
+def test_parse_date():
+    d = iso8601.parse_date("2006-10-20T15:34:56Z")
+    assert d.year == 2006
+    assert d.month == 10
+    assert d.day == 20
+    assert d.hour == 15
+    assert d.minute == 34
+    assert d.second == 56
+    assert d.tzinfo == iso8601.UTC
+
+def test_parse_date_fraction():
+    d = iso8601.parse_date("2006-10-20T15:34:56.123Z")
+    assert d.year == 2006
+    assert d.month == 10
+    assert d.day == 20
+    assert d.hour == 15
+    assert d.minute == 34
+    assert d.second == 56
+    assert d.microsecond == 123000
+    assert d.tzinfo == iso8601.UTC
+
+def test_parse_date_fraction_2():
+    """From bug 6
+    
+    """
+    d = iso8601.parse_date("2007-5-7T11:43:55.328Z'")
+    assert d.year == 2007
+    assert d.month == 5
+    assert d.day == 7
+    assert d.hour == 11
+    assert d.minute == 43
+    assert d.second == 55
+    assert d.microsecond == 328000
+    assert d.tzinfo == iso8601.UTC
+
+def test_parse_date_tz():
+    d = iso8601.parse_date("2006-10-20T15:34:56.123+02:30")
+    assert d.year == 2006
+    assert d.month == 10
+    assert d.day == 20
+    assert d.hour == 15
+    assert d.minute == 34
+    assert d.second == 56
+    assert d.microsecond == 123000
+    assert d.tzinfo.tzname(None) == "+02:30"
+    offset = d.tzinfo.utcoffset(None)
+    assert offset.days == 0
+    assert offset.seconds == 60 * 60 * 2.5
+
+def test_parse_invalid_date():
+    try:
+        iso8601.parse_date(None)
+    except iso8601.ParseError:
+        pass
+    else:
+        assert 1 == 2
+
+def test_parse_invalid_date2():
+    try:
+        iso8601.parse_date("23")
+    except iso8601.ParseError:
+        pass
+    else:
+        assert 1 == 2
+
+def test_parse_no_timezone():
+    """issue 4 - Handle datetime string without timezone
+    
+    This tests what happens when you parse a date with no timezone. While not
+    strictly correct this is quite common. I'll assume UTC for the time zone
+    in this case.
+    """
+    d = iso8601.parse_date("2007-01-01T08:00:00")
+    assert d.year == 2007
+    assert d.month == 1
+    assert d.day == 1
+    assert d.hour == 8
+    assert d.minute == 0
+    assert d.second == 0
+    assert d.microsecond == 0
+    assert d.tzinfo == iso8601.UTC
+
+def test_parse_no_timezone_different_default():
+    tz = iso8601.FixedOffset(2, 0, "test offset")
+    d = iso8601.parse_date("2007-01-01T08:00:00", default_timezone=tz)
+    assert d.tzinfo == tz
+
+def test_space_separator():
+    """Handle a separator other than T
+    
+    """
+    d = iso8601.parse_date("2007-06-23 06:40:34.00Z")
+    assert d.year == 2007
+    assert d.month == 6
+    assert d.day == 23
+    assert d.hour == 6
+    assert d.minute == 40
+    assert d.second == 34
+    assert d.microsecond == 0
+    assert d.tzinfo == iso8601.UTC
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..cdb61ec
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,58 @@
+try:
+    from setuptools import setup
+except ImportError:
+    from distutils import setup
+
+long_description="""Simple module to parse ISO 8601 dates
+
+This module parses the most common forms of ISO 8601 date strings (e.g.
+2007-01-14T20:34:22+00:00) into datetime objects.
+
+>>> import iso8601
+>>> iso8601.parse_date("2007-01-25T12:00:00Z")
+datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>)
+>>>
+
+Changes
+=======
+
+0.1.4
+-----
+
+* The default_timezone argument wasn't being passed through correctly,
+  UTC was being used in every case. Fixes issue 10.
+
+0.1.3
+-----
+
+* Fixed the microsecond handling, the generated microsecond values were 
+  way too small. Fixes issue 9.
+
+0.1.2
+-----
+
+* Adding ParseError to __all__ in iso8601 module, allows people to import it.
+  Addresses issue 7.
+* Be a little more flexible when dealing with dates without leading zeroes.
+  This violates the spec a little, but handles more dates as seen in the 
+  field. Addresses issue 6.
+* Allow date/time separators other than T.
+
+0.1.1
+-----
+
+* When parsing dates without a timezone the specified default is used. If no
+  default is specified then UTC is used. Addresses issue 4.
+"""
+
+setup(
+    name="iso8601",
+    version="0.1.4",
+    description=long_description.split("\n")[0],
+    long_description=long_description,
+    author="Michael Twomey",
+    author_email="micktwomey+iso8601@gmail.com",
+    url="http://code.google.com/p/pyiso8601/",
+    packages=["iso8601"],
+    license="MIT",
+)

Benjamin Mako Hill || Want to submit a patch?