Import Upstream version 0.5.4

This commit is contained in:
Emmanuel Cazenave 2018-06-22 13:09:12 +02:00
commit 45a2b87a8f
28 changed files with 3646 additions and 0 deletions

92
CHANGES.txt Normal file
View File

@ -0,0 +1,92 @@
CHANGES
=======
0.5.4 (2015-08-06)
------------------
- Fix parsing of Periods (Fabien Bochu)
- Make Duration objects hashable (Geoffrey Fairchild)
- Add multiplication to duration (Reinoud Elhorst)
0.5.1 (2014-11-07)
------------------
- fixed pickling of Duration objects
- raise ISO8601Error when there is no 'T' separator in datetime strings (Adrian Coveney)
0.5.0 (2014-02-23)
------------------
- ISO8601Error are subclasses of ValueError now (Michael Hrivnak)
- improve compatibility across various python variants and versions
- raise exceptions when using fractional years and months in date
maths with durations
- renamed method todatetime on Duraction objects to totimedelta
0.4.9 (2012-10-30)
------------------
- support pickling FixedOffset instances
- make sure parsed fractional seconds are in microseconds
- add leading zeros when formattig microseconds (Jarom Loveridge)
0.4.8 (2012-05-04)
------------------
- fixed incompatibility of unittests with python 2.5 and 2.6 (runs fine on 2.7
and 3.2)
0.4.7 (2012-01-26)
------------------
- fixed tzinfo formatting (never pass None into tzinfo.utcoffset())
0.4.6 (2012-01-06)
------------------
- added Python 3 compatibility via 2to3
0.4.5 (2012-01-06)
------------------
- made setuptools dependency optional
0.4.4 (2011-04-16)
------------------
- Fixed formatting of microseconds for datetime objects
0.4.3 (2010-10-29)
------------------
- Fixed problem with %P formating and fractions (supplied by David Brooks)
0.4.2 (2010-10-28)
------------------
- Implemented unary - for Duration (supplied by David Brooks)
- Output fractional seconds with '%P' format. (partly supplied by David Brooks)
0.4.1 (2010-10-13)
------------------
- fixed bug in comparison between timedelta and Duration.
- fixed precision problem with microseconds (reported by Tommi Virtanen)
0.4.0 (2009-02-09)
------------------
- added method to parse ISO 8601 time zone strings
- added methods to create ISO 8601 conforming strings
0.3.0 (2009-1-05)
------------------
- Initial release

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include CHANGES.txt
include TODO.txt

273
PKG-INFO Normal file
View File

@ -0,0 +1,273 @@
Metadata-Version: 1.1
Name: isodate
Version: 0.5.4
Summary: An ISO 8601 date/time/duration parser and formatter
Home-page: http://cheeseshop.python.org/pypi/isodate
Author: Gerhard Weis
Author-email: gerhard.weis@proclos.com
License: BSD
Description:
ISO 8601 date/time parser
=========================
.. image:: https://travis-ci.org/gweis/isodate.svg?branch=master
:target: https://travis-ci.org/gweis/isodate
:alt: Travis-CI
.. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master
:target: https://coveralls.io/r/gweis/isodate?branch=master
:alt: Coveralls
.. image:: https://pypip.in/version/isodate/badge.svg
:target: https://pypi.python.org/pypi/isodate/
:alt: Latest Version
.. image:: https://pypip.in/download/isodate/badge.svg
:target: https://pypi.python.org/pypi/isodate/
:alt: Downloads
.. image:: https://pypip.in/license/isodate/badge.svg
:target: https://pypi.python.org/pypi/isodate/
:alt: License
This module implements ISO 8601 date, time and duration parsing.
The implementation follows ISO8601:2004 standard, and implements only
date/time representations mentioned in the standard. If something is not
mentioned there, then it is treated as non existent, and not as an allowed
option.
For instance, ISO8601:2004 never mentions 2 digit years. So, it is not
intended by this module to support 2 digit years. (while it may still
be valid as ISO date, because it is not explicitly forbidden.)
Another example is, when no time zone information is given for a time,
then it should be interpreted as local time, and not UTC.
As this module maps ISO 8601 dates/times to standard Python data types, like
*date*, *time*, *datetime* and *timedelta*, it is not possible to convert
all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are
not allowed by the Python *date* and *datetime* classes. Additionally
fractional seconds are limited to microseconds. That means if the parser finds
for instance nanoseconds it will round it to microseconds.
Documentation
-------------
Currently there are four parsing methods available.
* parse_time:
parses an ISO 8601 time string into a *time* object
* parse_date:
parses an ISO 8601 date string into a *date* object
* parse_datetime:
parses an ISO 8601 date-time string into a *datetime* object
* parse_duration:
parses an ISO 8601 duration string into a *timedelta* or *Duration*
object.
* parse_tzinfo:
parses the time zone info part of an ISO 8601 string into a
*tzinfo* object.
As ISO 8601 allows to define durations in years and months, and *timedelta*
does not handle years and months, this module provides a *Duration* class,
which can be used almost like a *timedelta* object (with some limitations).
However, a *Duration* object can be converted into a *timedelta* object.
There are also ISO formatting methods for all supported data types. Each
*xxx_isoformat* method accepts a format parameter. The default format is
always the ISO 8601 expanded format. This is the same format used by
*datetime.isoformat*:
* time_isoformat:
Intended to create ISO time strings with default format
*hh:mm:ssZ*.
* date_isoformat:
Intended to create ISO date strings with default format
*yyyy-mm-dd*.
* datetime_isoformat:
Intended to create ISO date-time strings with default format
*yyyy-mm-ddThh:mm:ssZ*.
* duration_isoformat:
Intended to create ISO duration strings with default format
*PnnYnnMnnDTnnHnnMnnS*.
* tz_isoformat:
Intended to create ISO time zone strings with default format
*hh:mm*.
* strftime:
A re-implementation mostly compatible with Python's *strftime*, but
supports only those format strings, which can also be used for dates
prior 1900. This method also understands how to format *datetime* and
*Duration* instances.
Installation:
-------------
This module can easily be installed with Python standard installation methods.
Either use *python setup.py install* or in case you have *setuptools* or
*distribute* available, you can also use *easy_install*.
Limitations:
------------
* The parser accepts several date/time representation which should be invalid
according to ISO 8601 standard.
1. for date and time together, this parser accepts a mixture of basic and extended format.
e.g. the date could be in basic format, while the time is accepted in extended format.
It also allows short dates and times in date-time strings.
2. For incomplete dates, the first day is chosen. e.g. 19th century results in a date of
1901-01-01.
3. negative *Duration* and *timedelta* value are not fully supported yet.
Further information:
--------------------
The doc strings and unit tests should provide rather detailed information about
the methods and their limitations.
The source release provides a *setup.py* script and a *buildout.cfg*. Both can
be used to run the unit tests included.
Source code is available at `<http://github.com/gweis/isodate>`_.
CHANGES
=======
0.5.4 (2015-08-06)
------------------
- Fix parsing of Periods (Fabien Bochu)
- Make Duration objects hashable (Geoffrey Fairchild)
- Add multiplication to duration (Reinoud Elhorst)
0.5.1 (2014-11-07)
------------------
- fixed pickling of Duration objects
- raise ISO8601Error when there is no 'T' separator in datetime strings (Adrian Coveney)
0.5.0 (2014-02-23)
------------------
- ISO8601Error are subclasses of ValueError now (Michael Hrivnak)
- improve compatibility across various python variants and versions
- raise exceptions when using fractional years and months in date
maths with durations
- renamed method todatetime on Duraction objects to totimedelta
0.4.9 (2012-10-30)
------------------
- support pickling FixedOffset instances
- make sure parsed fractional seconds are in microseconds
- add leading zeros when formattig microseconds (Jarom Loveridge)
0.4.8 (2012-05-04)
------------------
- fixed incompatibility of unittests with python 2.5 and 2.6 (runs fine on 2.7
and 3.2)
0.4.7 (2012-01-26)
------------------
- fixed tzinfo formatting (never pass None into tzinfo.utcoffset())
0.4.6 (2012-01-06)
------------------
- added Python 3 compatibility via 2to3
0.4.5 (2012-01-06)
------------------
- made setuptools dependency optional
0.4.4 (2011-04-16)
------------------
- Fixed formatting of microseconds for datetime objects
0.4.3 (2010-10-29)
------------------
- Fixed problem with %P formating and fractions (supplied by David Brooks)
0.4.2 (2010-10-28)
------------------
- Implemented unary - for Duration (supplied by David Brooks)
- Output fractional seconds with '%P' format. (partly supplied by David Brooks)
0.4.1 (2010-10-13)
------------------
- fixed bug in comparison between timedelta and Duration.
- fixed precision problem with microseconds (reported by Tommi Virtanen)
0.4.0 (2009-02-09)
------------------
- added method to parse ISO 8601 time zone strings
- added methods to create ISO 8601 conforming strings
0.3.0 (2009-1-05)
------------------
- Initial release
TODOs
=====
This to do list contains some thoughts and ideas about missing features, and
parts to think about, whether to implement them or not. This list is probably
not complete.
Missing features:
-----------------
* time formating does not allow to create fractional representations.
* parser for ISO intervals.
* currently microseconds are always padded to a length of 6 characters.
trailing 0s should be optional
Documentation:
--------------
* parse_datetime:
- complete documentation to show what this function allows, but ISO forbids.
and vice verse.
- support other separators between date and time than 'T'
* parse_date:
- yeardigits should be always greater than 4
- dates before 0001-01-01 are not supported
* parse_duration:
- alternative formats are not fully supported due to parse_date restrictions
- standard duration format is fully supported but not very restrictive.
* Duration:
- support fractional years and month in calculations
- implement w3c order relation? (`<http://www.w3.org/TR/xmlschema-2/#duration-order>`_)
- refactor to have duration mathematics only at one place.
- localize __str__ method (does timedelta do this?)
- when is a Duration negative?
- normalize Durations. months [00-12] and years ]-inf,+inf[
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Internet
Classifier: Topic :: Software Development :: Libraries :: Python Modules

119
README.rst Normal file
View File

@ -0,0 +1,119 @@
ISO 8601 date/time parser
=========================
.. image:: https://travis-ci.org/gweis/isodate.svg?branch=master
:target: https://travis-ci.org/gweis/isodate
:alt: Travis-CI
.. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master
:target: https://coveralls.io/r/gweis/isodate?branch=master
:alt: Coveralls
.. image:: https://pypip.in/version/isodate/badge.svg
:target: https://pypi.python.org/pypi/isodate/
:alt: Latest Version
.. image:: https://pypip.in/download/isodate/badge.svg
:target: https://pypi.python.org/pypi/isodate/
:alt: Downloads
.. image:: https://pypip.in/license/isodate/badge.svg
:target: https://pypi.python.org/pypi/isodate/
:alt: License
This module implements ISO 8601 date, time and duration parsing.
The implementation follows ISO8601:2004 standard, and implements only
date/time representations mentioned in the standard. If something is not
mentioned there, then it is treated as non existent, and not as an allowed
option.
For instance, ISO8601:2004 never mentions 2 digit years. So, it is not
intended by this module to support 2 digit years. (while it may still
be valid as ISO date, because it is not explicitly forbidden.)
Another example is, when no time zone information is given for a time,
then it should be interpreted as local time, and not UTC.
As this module maps ISO 8601 dates/times to standard Python data types, like
*date*, *time*, *datetime* and *timedelta*, it is not possible to convert
all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are
not allowed by the Python *date* and *datetime* classes. Additionally
fractional seconds are limited to microseconds. That means if the parser finds
for instance nanoseconds it will round it to microseconds.
Documentation
-------------
Currently there are four parsing methods available.
* parse_time:
parses an ISO 8601 time string into a *time* object
* parse_date:
parses an ISO 8601 date string into a *date* object
* parse_datetime:
parses an ISO 8601 date-time string into a *datetime* object
* parse_duration:
parses an ISO 8601 duration string into a *timedelta* or *Duration*
object.
* parse_tzinfo:
parses the time zone info part of an ISO 8601 string into a
*tzinfo* object.
As ISO 8601 allows to define durations in years and months, and *timedelta*
does not handle years and months, this module provides a *Duration* class,
which can be used almost like a *timedelta* object (with some limitations).
However, a *Duration* object can be converted into a *timedelta* object.
There are also ISO formatting methods for all supported data types. Each
*xxx_isoformat* method accepts a format parameter. The default format is
always the ISO 8601 expanded format. This is the same format used by
*datetime.isoformat*:
* time_isoformat:
Intended to create ISO time strings with default format
*hh:mm:ssZ*.
* date_isoformat:
Intended to create ISO date strings with default format
*yyyy-mm-dd*.
* datetime_isoformat:
Intended to create ISO date-time strings with default format
*yyyy-mm-ddThh:mm:ssZ*.
* duration_isoformat:
Intended to create ISO duration strings with default format
*PnnYnnMnnDTnnHnnMnnS*.
* tz_isoformat:
Intended to create ISO time zone strings with default format
*hh:mm*.
* strftime:
A re-implementation mostly compatible with Python's *strftime*, but
supports only those format strings, which can also be used for dates
prior 1900. This method also understands how to format *datetime* and
*Duration* instances.
Installation:
-------------
This module can easily be installed with Python standard installation methods.
Either use *python setup.py install* or in case you have *setuptools* or
*distribute* available, you can also use *easy_install*.
Limitations:
------------
* The parser accepts several date/time representation which should be invalid
according to ISO 8601 standard.
1. for date and time together, this parser accepts a mixture of basic and extended format.
e.g. the date could be in basic format, while the time is accepted in extended format.
It also allows short dates and times in date-time strings.
2. For incomplete dates, the first day is chosen. e.g. 19th century results in a date of
1901-01-01.
3. negative *Duration* and *timedelta* value are not fully supported yet.
Further information:
--------------------
The doc strings and unit tests should provide rather detailed information about
the methods and their limitations.
The source release provides a *setup.py* script and a *buildout.cfg*. Both can
be used to run the unit tests included.
Source code is available at `<http://github.com/gweis/isodate>`_.

39
TODO.txt Normal file
View File

@ -0,0 +1,39 @@
TODOs
=====
This to do list contains some thoughts and ideas about missing features, and
parts to think about, whether to implement them or not. This list is probably
not complete.
Missing features:
-----------------
* time formating does not allow to create fractional representations.
* parser for ISO intervals.
* currently microseconds are always padded to a length of 6 characters.
trailing 0s should be optional
Documentation:
--------------
* parse_datetime:
- complete documentation to show what this function allows, but ISO forbids.
and vice verse.
- support other separators between date and time than 'T'
* parse_date:
- yeardigits should be always greater than 4
- dates before 0001-01-01 are not supported
* parse_duration:
- alternative formats are not fully supported due to parse_date restrictions
- standard duration format is fully supported but not very restrictive.
* Duration:
- support fractional years and month in calculations
- implement w3c order relation? (`<http://www.w3.org/TR/xmlschema-2/#duration-order>`_)
- refactor to have duration mathematics only at one place.
- localize __str__ method (does timedelta do this?)
- when is a Duration negative?
- normalize Durations. months [00-12] and years ]-inf,+inf[

5
setup.cfg Normal file
View File

@ -0,0 +1,5 @@
[egg_info]
tag_build =
tag_svn_revision = 0
tag_date = 0

84
setup.py Normal file
View File

@ -0,0 +1,84 @@
#!/usr/bin/env python
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
import os
import sys
setupargs = {}
try:
from setuptools import setup
setupargs['test_suite'] = 'isodate.tests.test_suite'
if sys.version[0] == '3':
setupargs['use_2to3'] = True
except ImportError:
from distutils.core import setup
if sys.version[0] == '3':
from distutils.command.build_py import build_py_2to3
setupargs['cmdclass'] = {'build_py': build_py_2to3}
def read(*rnames):
return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
setup(name='isodate',
version='0.5.4',
packages=['isodate', 'isodate.tests'],
package_dir={'': 'src'},
# dependencies:
# install_requires = [],
# PyPI metadata
author='Gerhard Weis',
author_email='gerhard.weis@proclos.com',
description='An ISO 8601 date/time/duration parser and formatter',
license='BSD',
# keywords = '',
url='http://cheeseshop.python.org/pypi/isodate',
long_description=(read('README.rst') +
read('CHANGES.txt') +
read('TODO.txt')),
classifiers=['Development Status :: 4 - Beta',
# 'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Internet',
('Topic :: Software Development :'
': Libraries :: Python Modules'),
],
**setupargs)

View File

@ -0,0 +1,273 @@
Metadata-Version: 1.1
Name: isodate
Version: 0.5.4
Summary: An ISO 8601 date/time/duration parser and formatter
Home-page: http://cheeseshop.python.org/pypi/isodate
Author: Gerhard Weis
Author-email: gerhard.weis@proclos.com
License: BSD
Description:
ISO 8601 date/time parser
=========================
.. image:: https://travis-ci.org/gweis/isodate.svg?branch=master
:target: https://travis-ci.org/gweis/isodate
:alt: Travis-CI
.. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master
:target: https://coveralls.io/r/gweis/isodate?branch=master
:alt: Coveralls
.. image:: https://pypip.in/version/isodate/badge.svg
:target: https://pypi.python.org/pypi/isodate/
:alt: Latest Version
.. image:: https://pypip.in/download/isodate/badge.svg
:target: https://pypi.python.org/pypi/isodate/
:alt: Downloads
.. image:: https://pypip.in/license/isodate/badge.svg
:target: https://pypi.python.org/pypi/isodate/
:alt: License
This module implements ISO 8601 date, time and duration parsing.
The implementation follows ISO8601:2004 standard, and implements only
date/time representations mentioned in the standard. If something is not
mentioned there, then it is treated as non existent, and not as an allowed
option.
For instance, ISO8601:2004 never mentions 2 digit years. So, it is not
intended by this module to support 2 digit years. (while it may still
be valid as ISO date, because it is not explicitly forbidden.)
Another example is, when no time zone information is given for a time,
then it should be interpreted as local time, and not UTC.
As this module maps ISO 8601 dates/times to standard Python data types, like
*date*, *time*, *datetime* and *timedelta*, it is not possible to convert
all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are
not allowed by the Python *date* and *datetime* classes. Additionally
fractional seconds are limited to microseconds. That means if the parser finds
for instance nanoseconds it will round it to microseconds.
Documentation
-------------
Currently there are four parsing methods available.
* parse_time:
parses an ISO 8601 time string into a *time* object
* parse_date:
parses an ISO 8601 date string into a *date* object
* parse_datetime:
parses an ISO 8601 date-time string into a *datetime* object
* parse_duration:
parses an ISO 8601 duration string into a *timedelta* or *Duration*
object.
* parse_tzinfo:
parses the time zone info part of an ISO 8601 string into a
*tzinfo* object.
As ISO 8601 allows to define durations in years and months, and *timedelta*
does not handle years and months, this module provides a *Duration* class,
which can be used almost like a *timedelta* object (with some limitations).
However, a *Duration* object can be converted into a *timedelta* object.
There are also ISO formatting methods for all supported data types. Each
*xxx_isoformat* method accepts a format parameter. The default format is
always the ISO 8601 expanded format. This is the same format used by
*datetime.isoformat*:
* time_isoformat:
Intended to create ISO time strings with default format
*hh:mm:ssZ*.
* date_isoformat:
Intended to create ISO date strings with default format
*yyyy-mm-dd*.
* datetime_isoformat:
Intended to create ISO date-time strings with default format
*yyyy-mm-ddThh:mm:ssZ*.
* duration_isoformat:
Intended to create ISO duration strings with default format
*PnnYnnMnnDTnnHnnMnnS*.
* tz_isoformat:
Intended to create ISO time zone strings with default format
*hh:mm*.
* strftime:
A re-implementation mostly compatible with Python's *strftime*, but
supports only those format strings, which can also be used for dates
prior 1900. This method also understands how to format *datetime* and
*Duration* instances.
Installation:
-------------
This module can easily be installed with Python standard installation methods.
Either use *python setup.py install* or in case you have *setuptools* or
*distribute* available, you can also use *easy_install*.
Limitations:
------------
* The parser accepts several date/time representation which should be invalid
according to ISO 8601 standard.
1. for date and time together, this parser accepts a mixture of basic and extended format.
e.g. the date could be in basic format, while the time is accepted in extended format.
It also allows short dates and times in date-time strings.
2. For incomplete dates, the first day is chosen. e.g. 19th century results in a date of
1901-01-01.
3. negative *Duration* and *timedelta* value are not fully supported yet.
Further information:
--------------------
The doc strings and unit tests should provide rather detailed information about
the methods and their limitations.
The source release provides a *setup.py* script and a *buildout.cfg*. Both can
be used to run the unit tests included.
Source code is available at `<http://github.com/gweis/isodate>`_.
CHANGES
=======
0.5.4 (2015-08-06)
------------------
- Fix parsing of Periods (Fabien Bochu)
- Make Duration objects hashable (Geoffrey Fairchild)
- Add multiplication to duration (Reinoud Elhorst)
0.5.1 (2014-11-07)
------------------
- fixed pickling of Duration objects
- raise ISO8601Error when there is no 'T' separator in datetime strings (Adrian Coveney)
0.5.0 (2014-02-23)
------------------
- ISO8601Error are subclasses of ValueError now (Michael Hrivnak)
- improve compatibility across various python variants and versions
- raise exceptions when using fractional years and months in date
maths with durations
- renamed method todatetime on Duraction objects to totimedelta
0.4.9 (2012-10-30)
------------------
- support pickling FixedOffset instances
- make sure parsed fractional seconds are in microseconds
- add leading zeros when formattig microseconds (Jarom Loveridge)
0.4.8 (2012-05-04)
------------------
- fixed incompatibility of unittests with python 2.5 and 2.6 (runs fine on 2.7
and 3.2)
0.4.7 (2012-01-26)
------------------
- fixed tzinfo formatting (never pass None into tzinfo.utcoffset())
0.4.6 (2012-01-06)
------------------
- added Python 3 compatibility via 2to3
0.4.5 (2012-01-06)
------------------
- made setuptools dependency optional
0.4.4 (2011-04-16)
------------------
- Fixed formatting of microseconds for datetime objects
0.4.3 (2010-10-29)
------------------
- Fixed problem with %P formating and fractions (supplied by David Brooks)
0.4.2 (2010-10-28)
------------------
- Implemented unary - for Duration (supplied by David Brooks)
- Output fractional seconds with '%P' format. (partly supplied by David Brooks)
0.4.1 (2010-10-13)
------------------
- fixed bug in comparison between timedelta and Duration.
- fixed precision problem with microseconds (reported by Tommi Virtanen)
0.4.0 (2009-02-09)
------------------
- added method to parse ISO 8601 time zone strings
- added methods to create ISO 8601 conforming strings
0.3.0 (2009-1-05)
------------------
- Initial release
TODOs
=====
This to do list contains some thoughts and ideas about missing features, and
parts to think about, whether to implement them or not. This list is probably
not complete.
Missing features:
-----------------
* time formating does not allow to create fractional representations.
* parser for ISO intervals.
* currently microseconds are always padded to a length of 6 characters.
trailing 0s should be optional
Documentation:
--------------
* parse_datetime:
- complete documentation to show what this function allows, but ISO forbids.
and vice verse.
- support other separators between date and time than 'T'
* parse_date:
- yeardigits should be always greater than 4
- dates before 0001-01-01 are not supported
* parse_duration:
- alternative formats are not fully supported due to parse_date restrictions
- standard duration format is fully supported but not very restrictive.
* Duration:
- support fractional years and month in calculations
- implement w3c order relation? (`<http://www.w3.org/TR/xmlschema-2/#duration-order>`_)
- refactor to have duration mathematics only at one place.
- localize __str__ method (does timedelta do this?)
- when is a Duration negative?
- normalize Durations. months [00-12] and years ]-inf,+inf[
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Internet
Classifier: Topic :: Software Development :: Libraries :: Python Modules

View File

@ -0,0 +1,26 @@
CHANGES.txt
MANIFEST.in
README.rst
TODO.txt
setup.py
src/isodate/__init__.py
src/isodate/duration.py
src/isodate/isodates.py
src/isodate/isodatetime.py
src/isodate/isoduration.py
src/isodate/isoerror.py
src/isodate/isostrf.py
src/isodate/isotime.py
src/isodate/isotzinfo.py
src/isodate/tzinfo.py
src/isodate.egg-info/PKG-INFO
src/isodate.egg-info/SOURCES.txt
src/isodate.egg-info/dependency_links.txt
src/isodate.egg-info/top_level.txt
src/isodate/tests/__init__.py
src/isodate/tests/test_date.py
src/isodate/tests/test_datetime.py
src/isodate/tests/test_duration.py
src/isodate/tests/test_pickle.py
src/isodate/tests/test_strf.py
src/isodate/tests/test_time.py

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
isodate

70
src/isodate/__init__.py Normal file
View File

@ -0,0 +1,70 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Import all essential functions and constants to re-export them here for easy
access.
This module contains also various pre-defined ISO 8601 format strings.
'''
from isodate.isodates import parse_date, date_isoformat
from isodate.isotime import parse_time, time_isoformat
from isodate.isodatetime import parse_datetime, datetime_isoformat
from isodate.isoduration import parse_duration, duration_isoformat
from isodate.isoerror import ISO8601Error
from isodate.isotzinfo import parse_tzinfo, tz_isoformat
from isodate.tzinfo import UTC, FixedOffset, LOCAL
from isodate.duration import Duration
from isodate.isostrf import strftime
from isodate.isostrf import DATE_BAS_COMPLETE, DATE_BAS_ORD_COMPLETE
from isodate.isostrf import DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE
from isodate.isostrf import DATE_CENTURY, DATE_EXT_COMPLETE
from isodate.isostrf import DATE_EXT_ORD_COMPLETE, DATE_EXT_WEEK
from isodate.isostrf import DATE_EXT_WEEK_COMPLETE, DATE_MONTH, DATE_YEAR
from isodate.isostrf import TIME_BAS_COMPLETE, TIME_BAS_MINUTE
from isodate.isostrf import TIME_EXT_COMPLETE, TIME_EXT_MINUTE
from isodate.isostrf import TIME_HOUR
from isodate.isostrf import TZ_BAS, TZ_EXT, TZ_HOUR
from isodate.isostrf import DT_BAS_COMPLETE, DT_EXT_COMPLETE
from isodate.isostrf import DT_BAS_ORD_COMPLETE, DT_EXT_ORD_COMPLETE
from isodate.isostrf import DT_BAS_WEEK_COMPLETE, DT_EXT_WEEK_COMPLETE
from isodate.isostrf import D_DEFAULT, D_WEEK, D_ALT_EXT, D_ALT_BAS
from isodate.isostrf import D_ALT_BAS_ORD, D_ALT_EXT_ORD
__all__ = ['parse_date', 'date_isoformat', 'parse_time', 'time_isoformat',
'parse_datetime', 'datetime_isoformat', 'parse_duration',
'duration_isoformat', 'ISO8601Error', 'parse_tzinfo',
'tz_isoformat', 'UTC', 'FixedOffset', 'LOCAL', 'Duration',
'strftime', 'DATE_BAS_COMPLETE', 'DATE_BAS_ORD_COMPLETE',
'DATE_BAS_WEEK', 'DATE_BAS_WEEK_COMPLETE', 'DATE_CENTURY',
'DATE_EXT_COMPLETE', 'DATE_EXT_ORD_COMPLETE', 'DATE_EXT_WEEK',
'DATE_EXT_WEEK_COMPLETE', 'DATE_MONTH', 'DATE_YEAR',
'TIME_BAS_COMPLETE', 'TIME_BAS_MINUTE', 'TIME_EXT_COMPLETE',
'TIME_EXT_MINUTE', 'TIME_HOUR', 'TZ_BAS', 'TZ_EXT', 'TZ_HOUR',
'DT_BAS_COMPLETE', 'DT_EXT_COMPLETE', 'DT_BAS_ORD_COMPLETE',
'DT_EXT_ORD_COMPLETE', 'DT_BAS_WEEK_COMPLETE',
'DT_EXT_WEEK_COMPLETE', 'D_DEFAULT', 'D_WEEK', 'D_ALT_EXT',
'D_ALT_BAS', 'D_ALT_BAS_ORD', 'D_ALT_EXT_ORD']

324
src/isodate/duration.py Normal file
View File

@ -0,0 +1,324 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module defines a Duration class.
The class Duration allows to define durations in years and months and can be
used as limited replacement for timedelta objects.
'''
from datetime import date, datetime, timedelta
from decimal import Decimal, ROUND_FLOOR
def fquotmod(val, low, high):
'''
A divmod function with boundaries.
'''
# assumes that all the maths is done with Decimals.
# divmod for Decimal uses truncate instead of floor as builtin
# divmod, so we have to do it manually here.
a, b = val - low, high - low
div = (a / b).to_integral(ROUND_FLOOR)
mod = a - div * b
# if we were not usig Decimal, it would look like this.
# div, mod = divmod(val - low, high - low)
mod += low
return int(div), mod
def max_days_in_month(year, month):
'''
Determines the number of days of a specific month in a specific year.
'''
if month in (1, 3, 5, 7, 8, 10, 12):
return 31
if month in (4, 6, 9, 11):
return 30
if ((year % 400) == 0) or ((year % 100) != 0) and ((year % 4) == 0):
return 29
return 28
class Duration(object):
'''
A class which represents a duration.
The difference to datetime.timedelta is, that this class handles also
differences given in years and months.
A Duration treats differences given in year, months separately from all
other components.
A Duration can be used almost like any timedelta object, however there
are some restrictions:
* It is not really possible to compare Durations, because it is unclear,
whether a duration of 1 year is bigger than 365 days or not.
* Equality is only tested between the two (year, month vs. timedelta)
basic components.
A Duration can also be converted into a datetime object, but this requires
a start date or an end date.
The algorithm to add a duration to a date is defined at
http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes
'''
def __init__(self, days=0, seconds=0, microseconds=0, milliseconds=0,
minutes=0, hours=0, weeks=0, months=0, years=0):
'''
Initialise this Duration instance with the given parameters.
'''
if not isinstance(months, Decimal):
months = Decimal(str(months))
if not isinstance(years, Decimal):
years = Decimal(str(years))
self.months = months
self.years = years
self.tdelta = timedelta(days, seconds, microseconds, milliseconds,
minutes, hours, weeks)
def __getstate__(self):
return self.__dict__
def __setstate__(self, state):
self.__dict__.update(state)
def __getattr__(self, name):
'''
Provide direct access to attributes of included timedelta instance.
'''
return getattr(self.tdelta, name)
def __str__(self):
'''
Return a string representation of this duration similar to timedelta.
'''
params = []
if self.years:
params.append('%d years' % self.years)
if self.months:
params.append('%d months' % self.months)
params.append(str(self.tdelta))
return ', '.join(params)
def __repr__(self):
'''
Return a string suitable for repr(x) calls.
'''
return "%s.%s(%d, %d, %d, years=%d, months=%d)" % (
self.__class__.__module__, self.__class__.__name__,
self.tdelta.days, self.tdelta.seconds,
self.tdelta.microseconds, self.years, self.months)
def __hash__(self):
'''
Return a hash of this instance so that it can be used in, for
example, dicts and sets.
'''
return hash((self.tdelta, self.months, self.years))
def __neg__(self):
"""
A simple unary minus.
Returns a new Duration instance with all it's negated.
"""
negduration = Duration(years=-self.years, months=-self.months)
negduration.tdelta = -self.tdelta
return negduration
def __add__(self, other):
'''
Durations can be added with Duration, timedelta, date and datetime
objects.
'''
if isinstance(other, timedelta):
newduration = Duration(years=self.years, months=self.months)
newduration.tdelta = self.tdelta + other
return newduration
if isinstance(other, Duration):
newduration = Duration(years=self.years + other.years,
months=self.months + other.months)
newduration.tdelta = self.tdelta + other.tdelta
return newduration
if isinstance(other, (date, datetime)):
if (not(float(self.years).is_integer() and
float(self.months).is_integer())):
raise ValueError('fractional years or months not supported'
' for date calculations')
newmonth = other.month + self.months
carry, newmonth = fquotmod(newmonth, 1, 13)
newyear = other.year + self.years + carry
maxdays = max_days_in_month(newyear, newmonth)
if other.day > maxdays:
newday = maxdays
else:
newday = other.day
newdt = other.replace(year=newyear, month=newmonth, day=newday)
return self.tdelta + newdt
raise TypeError('unsupported operand type(s) for +: %s and %s' %
(self.__class__, other.__class__))
def __radd__(self, other):
'''
Add durations to timedelta, date and datetime objects.
'''
if isinstance(other, timedelta):
newduration = Duration(years=self.years, months=self.months)
newduration.tdelta = self.tdelta + other
return newduration
if isinstance(other, (date, datetime)):
if (not(float(self.years).is_integer() and
float(self.months).is_integer())):
raise ValueError('fractional years or months not supported'
' for date calculations')
newmonth = other.month + self.months
carry, newmonth = fquotmod(newmonth, 1, 13)
newyear = other.year + self.years + carry
maxdays = max_days_in_month(newyear, newmonth)
if other.day > maxdays:
newday = maxdays
else:
newday = other.day
newdt = other.replace(year=newyear, month=newmonth, day=newday)
return newdt + self.tdelta
raise TypeError('unsupported operand type(s) for +: %s and %s' %
(other.__class__, self.__class__))
def __mul__(self, other):
if isinstance(other, int):
newduration = Duration(
years=self.years * other,
months=self.months * other)
newduration.tdelta = self.tdelta * other
return newduration
raise TypeError('unsupported operand type(s) for +: %s and %s' %
(self.__class__, other.__class__))
def __rmul__(self, other):
if isinstance(other, int):
newduration = Duration(
years=self.years * other,
months=self.months * other)
newduration.tdelta = self.tdelta * other
return newduration
raise TypeError('unsupported operand type(s) for +: %s and %s' %
(other.__class__, self.__class__))
def __sub__(self, other):
'''
It is possible to subtract Duration and timedelta objects from Duration
objects.
'''
if isinstance(other, Duration):
newduration = Duration(years=self.years - other.years,
months=self.months - other.months)
newduration.tdelta = self.tdelta - other.tdelta
return newduration
if isinstance(other, timedelta):
newduration = Duration(years=self.years, months=self.months)
newduration.tdelta = self.tdelta - other
return newduration
raise TypeError('unsupported operand type(s) for -: %s and %s' %
(self.__class__, other.__class__))
def __rsub__(self, other):
'''
It is possible to subtract Duration objecs from date, datetime and
timedelta objects.
'''
# print '__rsub__:', self, other
if isinstance(other, (date, datetime)):
if (not(float(self.years).is_integer() and
float(self.months).is_integer())):
raise ValueError('fractional years or months not supported'
' for date calculations')
newmonth = other.month - self.months
carry, newmonth = fquotmod(newmonth, 1, 13)
newyear = other.year - self.years + carry
maxdays = max_days_in_month(newyear, newmonth)
if other.day > maxdays:
newday = maxdays
else:
newday = other.day
newdt = other.replace(year=newyear, month=newmonth, day=newday)
return newdt - self.tdelta
if isinstance(other, timedelta):
tmpdur = Duration()
tmpdur.tdelta = other
return tmpdur - self
raise TypeError('unsupported operand type(s) for -: %s and %s' %
(other.__class__, self.__class__))
def __eq__(self, other):
'''
If the years, month part and the timedelta part are both equal, then
the two Durations are considered equal.
'''
if ((isinstance(other, timedelta) and
self.years == 0 and self.months == 0)):
return self.tdelta == other
if not isinstance(other, Duration):
return NotImplemented
if (((self.years * 12 + self.months) ==
(other.years * 12 + other.months) and
self.tdelta == other.tdelta)):
return True
return False
def __ne__(self, other):
'''
If the years, month part or the timedelta part is not equal, then
the two Durations are considered not equal.
'''
if ((isinstance(other, timedelta) and
self.years == 0 and
self.months == 0)):
return self.tdelta != other
if not isinstance(other, Duration):
return NotImplemented
if (((self.years * 12 + self.months) !=
(other.years * 12 + other.months) or
self.tdelta != other.tdelta)):
return True
return False
def totimedelta(self, start=None, end=None):
'''
Convert this duration into a timedelta object.
This method requires a start datetime or end datetimem, but raises
an exception if both are given.
'''
if start is None and end is None:
raise ValueError("start or end required")
if start is not None and end is not None:
raise ValueError("only start or end allowed")
if start is not None:
return (start + self) - start
return end - (end - self)

204
src/isodate/isodates.py Normal file
View File

@ -0,0 +1,204 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This modules provides a method to parse an ISO 8601:2004 date string to a
python datetime.date instance.
It supports all basic, extended and expanded formats as described in the ISO
standard. The only limitations it has, are given by the Python datetime.date
implementation, which does not support dates before 0001-01-01.
'''
import re
from datetime import date, timedelta
from isodate.isostrf import strftime, DATE_EXT_COMPLETE
from isodate.isoerror import ISO8601Error
DATE_REGEX_CACHE = {}
# A dictionary to cache pre-compiled regular expressions.
# A set of regular expressions is identified, by number of year digits allowed
# and whether a plus/minus sign is required or not. (This option is changeable
# only for 4 digit years).
def build_date_regexps(yeardigits=4, expanded=False):
'''
Compile set of regular expressions to parse ISO dates. The expressions will
be created only if they are not already in REGEX_CACHE.
It is necessary to fix the number of year digits, else it is not possible
to automatically distinguish between various ISO date formats.
ISO 8601 allows more than 4 digit years, on prior agreement, but then a +/-
sign is required (expanded format). To support +/- sign for 4 digit years,
the expanded parameter needs to be set to True.
'''
if yeardigits != 4:
expanded = True
if (yeardigits, expanded) not in DATE_REGEX_CACHE:
cache_entry = []
# ISO 8601 expanded DATE formats allow an arbitrary number of year
# digits with a leading +/- sign.
if expanded:
sign = 1
else:
sign = 0
# 1. complete dates:
# YYYY-MM-DD or +- YYYYYY-MM-DD... extended date format
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})"
% (sign, yeardigits)))
# YYYYMMDD or +- YYYYYYMMDD... basic date format
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"(?P<month>[0-9]{2})(?P<day>[0-9]{2})"
% (sign, yeardigits)))
# 2. complete week dates:
# YYYY-Www-D or +-YYYYYY-Www-D ... extended week date
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-W(?P<week>[0-9]{2})-(?P<day>[0-9]{1})"
% (sign, yeardigits)))
# YYYYWwwD or +-YYYYYYWwwD ... basic week date
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
r"(?P<week>[0-9]{2})(?P<day>[0-9]{1})"
% (sign, yeardigits)))
# 3. ordinal dates:
# YYYY-DDD or +-YYYYYY-DDD ... extended format
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-(?P<day>[0-9]{3})"
% (sign, yeardigits)))
# YYYYDDD or +-YYYYYYDDD ... basic format
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"(?P<day>[0-9]{3})"
% (sign, yeardigits)))
# 4. week dates:
# YYYY-Www or +-YYYYYY-Www ... extended reduced accuracy week date
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-W(?P<week>[0-9]{2})"
% (sign, yeardigits)))
# YYYYWww or +-YYYYYYWww ... basic reduced accuracy week date
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
r"(?P<week>[0-9]{2})"
% (sign, yeardigits)))
# 5. month dates:
# YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-(?P<month>[0-9]{2})"
% (sign, yeardigits)))
# 6. year dates:
# YYYY or +-YYYYYY ... reduced accuracy specific year
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
% (sign, yeardigits)))
# 7. century dates:
# YY or +-YYYY ... reduced accuracy specific century
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}"
r"(?P<century>[0-9]{%d})"
% (sign, yeardigits - 2)))
DATE_REGEX_CACHE[(yeardigits, expanded)] = cache_entry
return DATE_REGEX_CACHE[(yeardigits, expanded)]
def parse_date(datestring, yeardigits=4, expanded=False):
'''
Parse an ISO 8601 date string into a datetime.date object.
As the datetime.date implementation is limited to dates starting from
0001-01-01, negative dates (BC) and year 0 can not be parsed by this
method.
For incomplete dates, this method chooses the first day for it. For
instance if only a century is given, this method returns the 1st of
January in year 1 of this century.
supported formats: (expanded formats are shown with 6 digits for year)
YYYYMMDD +-YYYYYYMMDD basic complete date
YYYY-MM-DD +-YYYYYY-MM-DD extended complete date
YYYYWwwD +-YYYYYYWwwD basic complete week date
YYYY-Www-D +-YYYYYY-Www-D extended complete week date
YYYYDDD +-YYYYYYDDD basic ordinal date
YYYY-DDD +-YYYYYY-DDD extended ordinal date
YYYYWww +-YYYYYYWww basic incomplete week date
YYYY-Www +-YYYYYY-Www extended incomplete week date
YYY-MM +-YYYYYY-MM incomplete month date
YYYY +-YYYYYY incomplete year date
YY +-YYYY incomplete century date
@param datestring: the ISO date string to parse
@param yeardigits: how many digits are used to represent a year
@param expanded: if True then +/- signs are allowed. This parameter
is forced to True, if yeardigits != 4
@return: a datetime.date instance represented by datestring
@raise ISO8601Error: if this function can not parse the datestring
@raise ValueError: if datestring can not be represented by datetime.date
'''
if yeardigits != 4:
expanded = True
isodates = build_date_regexps(yeardigits, expanded)
for pattern in isodates:
match = pattern.match(datestring)
if match:
groups = match.groupdict()
# sign, century, year, month, week, day,
# FIXME: negative dates not possible with python standard types
sign = (groups['sign'] == '-' and -1) or 1
if 'century' in groups:
return date(sign * (int(groups['century']) * 100 + 1), 1, 1)
if 'month' not in groups: # weekdate or ordinal date
ret = date(sign * int(groups['year']), 1, 1)
if 'week' in groups:
isotuple = ret.isocalendar()
if 'day' in groups:
days = int(groups['day'] or 1)
else:
days = 1
# if first week in year, do weeks-1
return ret + timedelta(weeks=int(groups['week']) -
(((isotuple[1] == 1) and 1) or 0),
days=-isotuple[2] + days)
elif 'day' in groups: # ordinal date
return ret + timedelta(days=int(groups['day'])-1)
else: # year date
return ret
# year-, month-, or complete date
if 'day' not in groups or groups['day'] is None:
day = 1
else:
day = int(groups['day'])
return date(sign * int(groups['year']),
int(groups['month']) or 1, day)
raise ISO8601Error('Unrecognised ISO 8601 date format: %r' % datestring)
def date_isoformat(tdate, format=DATE_EXT_COMPLETE, yeardigits=4):
'''
Format date strings.
This method is just a wrapper around isodate.isostrf.strftime and uses
Date-Extended-Complete as default format.
'''
return strftime(tdate, format, yeardigits)

View File

@ -0,0 +1,68 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module defines a method to parse an ISO 8601:2004 date time string.
For this job it uses the parse_date and parse_time methods defined in date
and time module.
'''
from datetime import datetime
from isodate.isostrf import strftime
from isodate.isostrf import DATE_EXT_COMPLETE, TIME_EXT_COMPLETE, TZ_EXT
from isodate.isodates import parse_date
from isodate.isoerror import ISO8601Error
from isodate.isotime import parse_time
def parse_datetime(datetimestring):
'''
Parses ISO 8601 date-times into datetime.datetime objects.
This function uses parse_date and parse_time to do the job, so it allows
more combinations of date and time representations, than the actual
ISO 8601:2004 standard allows.
'''
try:
datestring, timestring = datetimestring.split('T')
except ValueError:
raise ISO8601Error("ISO 8601 time designator 'T' missing. Unable to"
" parse datetime string %r" % datetimestring)
tmpdate = parse_date(datestring)
tmptime = parse_time(timestring)
return datetime.combine(tmpdate, tmptime)
def datetime_isoformat(tdt, format=DATE_EXT_COMPLETE + 'T' +
TIME_EXT_COMPLETE + TZ_EXT):
'''
Format datetime strings.
This method is just a wrapper around isodate.isostrf.strftime and uses
Extended-Complete as default format.
'''
return strftime(tdt, format)

149
src/isodate/isoduration.py Normal file
View File

@ -0,0 +1,149 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module provides an ISO 8601:2004 duration parser.
It also provides a wrapper to strftime. This wrapper makes it easier to
format timedelta or Duration instances as ISO conforming strings.
'''
from datetime import timedelta
from decimal import Decimal
import re
from isodate.duration import Duration
from isodate.isoerror import ISO8601Error
from isodate.isodatetime import parse_datetime
from isodate.isostrf import strftime, D_DEFAULT
ISO8601_PERIOD_REGEX = re.compile(
r"^(?P<sign>[+-])?"
r"P(?!\b)"
r"(?P<years>[0-9]+([,.][0-9]+)?Y)?"
r"(?P<months>[0-9]+([,.][0-9]+)?M)?"
r"(?P<weeks>[0-9]+([,.][0-9]+)?W)?"
r"(?P<days>[0-9]+([,.][0-9]+)?D)?"
r"((?P<separator>T)(?P<hours>[0-9]+([,.][0-9]+)?H)?"
r"(?P<minutes>[0-9]+([,.][0-9]+)?M)?"
r"(?P<seconds>[0-9]+([,.][0-9]+)?S)?)?$")
# regular expression to parse ISO duartion strings.
def parse_duration(datestring):
"""
Parses an ISO 8601 durations into datetime.timedelta or Duration objects.
If the ISO date string does not contain years or months, a timedelta
instance is returned, else a Duration instance is returned.
The following duration formats are supported:
-PnnW duration in weeks
-PnnYnnMnnDTnnHnnMnnS complete duration specification
-PYYYYMMDDThhmmss basic alternative complete date format
-PYYYY-MM-DDThh:mm:ss extended alternative complete date format
-PYYYYDDDThhmmss basic alternative ordinal date format
-PYYYY-DDDThh:mm:ss extended alternative ordinal date format
The '-' is optional.
Limitations: ISO standard defines some restrictions about where to use
fractional numbers and which component and format combinations are
allowed. This parser implementation ignores all those restrictions and
returns something when it is able to find all necessary components.
In detail:
it does not check, whether only the last component has fractions.
it allows weeks specified with all other combinations
The alternative format does not support durations with years, months or
days set to 0.
"""
if not isinstance(datestring, basestring):
raise TypeError("Expecting a string %r" % datestring)
match = ISO8601_PERIOD_REGEX.match(datestring)
if not match:
# try alternative format:
if datestring.startswith("P"):
durdt = parse_datetime(datestring[1:])
if durdt.year != 0 or durdt.month != 0:
# create Duration
ret = Duration(days=durdt.day, seconds=durdt.second,
microseconds=durdt.microsecond,
minutes=durdt.minute, hours=durdt.hour,
months=durdt.month, years=durdt.year)
else: # FIXME: currently not possible in alternative format
# create timedelta
ret = timedelta(days=durdt.day, seconds=durdt.second,
microseconds=durdt.microsecond,
minutes=durdt.minute, hours=durdt.hour)
return ret
raise ISO8601Error("Unable to parse duration string %r" % datestring)
groups = match.groupdict()
for key, val in groups.items():
if key not in ('separator', 'sign'):
if val is None:
groups[key] = "0n"
# print groups[key]
if key in ('years', 'months'):
groups[key] = Decimal(groups[key][:-1].replace(',', '.'))
else:
# these values are passed into a timedelta object,
# which works with floats.
groups[key] = float(groups[key][:-1].replace(',', '.'))
if groups["years"] == 0 and groups["months"] == 0:
ret = timedelta(days=groups["days"], hours=groups["hours"],
minutes=groups["minutes"], seconds=groups["seconds"],
weeks=groups["weeks"])
if groups["sign"] == '-':
ret = timedelta(0) - ret
else:
ret = Duration(years=groups["years"], months=groups["months"],
days=groups["days"], hours=groups["hours"],
minutes=groups["minutes"], seconds=groups["seconds"],
weeks=groups["weeks"])
if groups["sign"] == '-':
ret = Duration(0) - ret
return ret
def duration_isoformat(tduration, format=D_DEFAULT):
'''
Format duration strings.
This method is just a wrapper around isodate.isostrf.strftime and uses
P%P (D_DEFAULT) as default format.
'''
# TODO: implement better decision for negative Durations.
# should be done in Duration class in consistent way with timedelta.
if (((isinstance(tduration, Duration) and
(tduration.years < 0 or tduration.months < 0 or
tduration.tdelta < timedelta(0))) or
(isinstance(tduration, timedelta) and
(tduration < timedelta(0))))):
ret = '-'
else:
ret = ''
ret += strftime(tduration, format)
return ret

33
src/isodate/isoerror.py Normal file
View File

@ -0,0 +1,33 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module defines all exception classes in the whole package.
'''
class ISO8601Error(ValueError):
'''Raised when the given ISO string can not be parsed.'''

213
src/isodate/isostrf.py Normal file
View File

@ -0,0 +1,213 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
"""
This module provides an alternative strftime method.
The strftime method in this module allows only a subset of Python's strftime
format codes, plus a few additional. It supports the full range of date values
possible with standard Python date/time objects. Furthermore there are several
pr-defined format strings in this module to make ease producing of ISO 8601
conforming strings.
"""
import re
from datetime import date, timedelta
from isodate.duration import Duration
from isodate.isotzinfo import tz_isoformat
# Date specific format strings
DATE_BAS_COMPLETE = '%Y%m%d'
DATE_EXT_COMPLETE = '%Y-%m-%d'
DATE_BAS_WEEK_COMPLETE = '%YW%W%w'
DATE_EXT_WEEK_COMPLETE = '%Y-W%W-%w'
DATE_BAS_ORD_COMPLETE = '%Y%j'
DATE_EXT_ORD_COMPLETE = '%Y-%j'
DATE_BAS_WEEK = '%YW%W'
DATE_EXT_WEEK = '%Y-W%W'
DATE_MONTH = '%Y-%m'
DATE_YEAR = '%Y'
DATE_CENTURY = '%C'
# Time specific format strings
TIME_BAS_COMPLETE = '%H%M%S'
TIME_EXT_COMPLETE = '%H:%M:%S'
TIME_BAS_MINUTE = '%H%M'
TIME_EXT_MINUTE = '%H:%M'
TIME_HOUR = '%H'
# Time zone formats
TZ_BAS = '%z'
TZ_EXT = '%Z'
TZ_HOUR = '%h'
# DateTime formats
DT_EXT_COMPLETE = DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + TZ_EXT
DT_BAS_COMPLETE = DATE_BAS_COMPLETE + 'T' + TIME_BAS_COMPLETE + TZ_BAS
DT_EXT_ORD_COMPLETE = DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_COMPLETE + TZ_EXT
DT_BAS_ORD_COMPLETE = DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_COMPLETE + TZ_BAS
DT_EXT_WEEK_COMPLETE = (DATE_EXT_WEEK_COMPLETE + 'T' +
TIME_EXT_COMPLETE + TZ_EXT)
DT_BAS_WEEK_COMPLETE = (DATE_BAS_WEEK_COMPLETE + 'T' +
TIME_BAS_COMPLETE + TZ_BAS)
# Duration formts
D_DEFAULT = 'P%P'
D_WEEK = 'P%p'
D_ALT_EXT = 'P' + DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE
D_ALT_BAS = 'P' + DATE_BAS_COMPLETE + 'T' + TIME_BAS_COMPLETE
D_ALT_EXT_ORD = 'P' + DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_COMPLETE
D_ALT_BAS_ORD = 'P' + DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_COMPLETE
STRF_DT_MAP = {'%d': lambda tdt, yds: '%02d' % tdt.day,
'%f': lambda tdt, yds: '%06d' % tdt.microsecond,
'%H': lambda tdt, yds: '%02d' % tdt.hour,
'%j': lambda tdt, yds: '%03d' % (tdt.toordinal() -
date(tdt.year,
1, 1).toordinal() +
1),
'%m': lambda tdt, yds: '%02d' % tdt.month,
'%M': lambda tdt, yds: '%02d' % tdt.minute,
'%S': lambda tdt, yds: '%02d' % tdt.second,
'%w': lambda tdt, yds: '%1d' % tdt.isoweekday(),
'%W': lambda tdt, yds: '%02d' % tdt.isocalendar()[1],
'%Y': lambda tdt, yds: (((yds != 4) and '+') or '') +
(('%%0%dd' % yds) % tdt.year),
'%C': lambda tdt, yds: (((yds != 4) and '+') or '') +
(('%%0%dd' % (yds - 2)) %
(tdt.year / 100)),
'%h': lambda tdt, yds: tz_isoformat(tdt, '%h'),
'%Z': lambda tdt, yds: tz_isoformat(tdt, '%Z'),
'%z': lambda tdt, yds: tz_isoformat(tdt, '%z'),
'%%': lambda tdt, yds: '%'}
STRF_D_MAP = {'%d': lambda tdt, yds: '%02d' % tdt.days,
'%f': lambda tdt, yds: '%06d' % tdt.microseconds,
'%H': lambda tdt, yds: '%02d' % (tdt.seconds / 60 / 60),
'%m': lambda tdt, yds: '%02d' % tdt.months,
'%M': lambda tdt, yds: '%02d' % ((tdt.seconds / 60) % 60),
'%S': lambda tdt, yds: '%02d' % (tdt.seconds % 60),
'%W': lambda tdt, yds: '%02d' % (abs(tdt.days / 7)),
'%Y': lambda tdt, yds: (((yds != 4) and '+') or '') +
(('%%0%dd' % yds) % tdt.years),
'%C': lambda tdt, yds: (((yds != 4) and '+') or '') +
(('%%0%dd' % (yds - 2)) %
(tdt.years / 100)),
'%%': lambda tdt, yds: '%'}
def _strfduration(tdt, format, yeardigits=4):
'''
this is the work method for timedelta and Duration instances.
see strftime for more details.
'''
def repl(match):
'''
lookup format command and return corresponding replacement.
'''
if match.group(0) in STRF_D_MAP:
return STRF_D_MAP[match.group(0)](tdt, yeardigits)
elif match.group(0) == '%P':
ret = []
if isinstance(tdt, Duration):
if tdt.years:
ret.append('%sY' % abs(tdt.years))
if tdt.months:
ret.append('%sM' % abs(tdt.months))
usecs = abs((tdt.days * 24 * 60 * 60 + tdt.seconds) * 1000000 +
tdt.microseconds)
seconds, usecs = divmod(usecs, 1000000)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24)
if days:
ret.append('%sD' % days)
if hours or minutes or seconds or usecs:
ret.append('T')
if hours:
ret.append('%sH' % hours)
if minutes:
ret.append('%sM' % minutes)
if seconds or usecs:
if usecs:
ret.append(("%d.%06d" % (seconds, usecs)).rstrip('0'))
else:
ret.append("%d" % seconds)
ret.append('S')
# at least one component has to be there.
return ret and ''.join(ret) or '0D'
elif match.group(0) == '%p':
return str(abs(tdt.days // 7)) + 'W'
return match.group(0)
return re.sub('%d|%f|%H|%m|%M|%S|%W|%Y|%C|%%|%P|%p', repl,
format)
def _strfdt(tdt, format, yeardigits=4):
'''
this is the work method for time and date instances.
see strftime for more details.
'''
def repl(match):
'''
lookup format command and return corresponding replacement.
'''
if match.group(0) in STRF_DT_MAP:
return STRF_DT_MAP[match.group(0)](tdt, yeardigits)
return match.group(0)
return re.sub('%d|%f|%H|%j|%m|%M|%S|%w|%W|%Y|%C|%z|%Z|%h|%%', repl,
format)
def strftime(tdt, format, yeardigits=4):
'''Directive Meaning Notes
%d Day of the month as a decimal number [01,31].
%f Microsecond as a decimal number [0,999999], zero-padded
on the left (1)
%H Hour (24-hour clock) as a decimal number [00,23].
%j Day of the year as a decimal number [001,366].
%m Month as a decimal number [01,12].
%M Minute as a decimal number [00,59].
%S Second as a decimal number [00,61]. (3)
%w Weekday as a decimal number [0(Monday),6].
%W Week number of the year (Monday as the first day of the week)
as a decimal number [00,53]. All days in a new year preceding the
first Monday are considered to be in week 0. (4)
%Y Year with century as a decimal number. [0000,9999]
%C Century as a decimal number. [00,99]
%z UTC offset in the form +HHMM or -HHMM (empty string if the
object is naive). (5)
%Z Time zone name (empty string if the object is naive).
%P ISO8601 duration format.
%p ISO8601 duration format in weeks.
%% A literal '%' character.
'''
if isinstance(tdt, (timedelta, Duration)):
return _strfduration(tdt, format, yeardigits)
return _strfdt(tdt, format, yeardigits)

158
src/isodate/isotime.py Normal file
View File

@ -0,0 +1,158 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This modules provides a method to parse an ISO 8601:2004 time string to a
Python datetime.time instance.
It supports all basic and extended formats including time zone specifications
as described in the ISO standard.
'''
import re
from decimal import Decimal
from datetime import time
from isodate.isostrf import strftime, TIME_EXT_COMPLETE, TZ_EXT
from isodate.isoerror import ISO8601Error
from isodate.isotzinfo import TZ_REGEX, build_tzinfo
TIME_REGEX_CACHE = []
# used to cache regular expressions to parse ISO time strings.
def build_time_regexps():
'''
Build regular expressions to parse ISO time string.
The regular expressions are compiled and stored in TIME_REGEX_CACHE
for later reuse.
'''
if not TIME_REGEX_CACHE:
# ISO 8601 time representations allow decimal fractions on least
# significant time component. Command and Full Stop are both valid
# fraction separators.
# The letter 'T' is allowed as time designator in front of a time
# expression.
# Immediately after a time expression, a time zone definition is
# allowed.
# a TZ may be missing (local time), be a 'Z' for UTC or a string of
# +-hh:mm where the ':mm' part can be skipped.
# TZ information patterns:
# ''
# Z
# +-hh:mm
# +-hhmm
# +-hh =>
# isotzinfo.TZ_REGEX
# 1. complete time:
# hh:mm:ss.ss ... extended format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}):"
r"(?P<minute>[0-9]{2}):"
r"(?P<second>[0-9]{2}"
r"([,.][0-9]+)?)" + TZ_REGEX))
# hhmmss.ss ... basic format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2})"
r"(?P<minute>[0-9]{2})"
r"(?P<second>[0-9]{2}"
r"([,.][0-9]+)?)" + TZ_REGEX))
# 2. reduced accuracy:
# hh:mm.mm ... extended format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}):"
r"(?P<minute>[0-9]{2}"
r"([,.][0-9]+)?)" + TZ_REGEX))
# hhmm.mm ... basic format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2})"
r"(?P<minute>[0-9]{2}"
r"([,.][0-9]+)?)" + TZ_REGEX))
# hh.hh ... basic format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}"
r"([,.][0-9]+)?)" + TZ_REGEX))
return TIME_REGEX_CACHE
def parse_time(timestring):
'''
Parses ISO 8601 times into datetime.time objects.
Following ISO 8601 formats are supported:
(as decimal separator a ',' or a '.' is allowed)
hhmmss.ssTZD basic complete time
hh:mm:ss.ssTZD extended compelte time
hhmm.mmTZD basic reduced accuracy time
hh:mm.mmTZD extended reduced accuracy time
hh.hhTZD basic reduced accuracy time
TZD is the time zone designator which can be in the following format:
no designator indicates local time zone
Z UTC
+-hhmm basic hours and minutes
+-hh:mm extended hours and minutes
+-hh hours
'''
isotimes = build_time_regexps()
for pattern in isotimes:
match = pattern.match(timestring)
if match:
groups = match.groupdict()
for key, value in groups.items():
if value is not None:
groups[key] = value.replace(',', '.')
tzinfo = build_tzinfo(groups['tzname'], groups['tzsign'],
int(groups['tzhour'] or 0),
int(groups['tzmin'] or 0))
if 'second' in groups:
# round to microseconds if fractional seconds are more precise
second = Decimal(groups['second']).quantize(Decimal('.000001'))
microsecond = (second - int(second)) * long(1e6)
# int(...) ... no rounding
# to_integral() ... rounding
return time(int(groups['hour']), int(groups['minute']),
int(second), int(microsecond.to_integral()),
tzinfo)
if 'minute' in groups:
minute = Decimal(groups['minute'])
second = (minute - int(minute)) * 60
microsecond = (second - int(second)) * long(1e6)
return time(int(groups['hour']), int(minute), int(second),
int(microsecond.to_integral()), tzinfo)
else:
microsecond, second, minute = 0, 0, 0
hour = Decimal(groups['hour'])
minute = (hour - int(hour)) * 60
second = (minute - int(minute)) * 60
microsecond = (second - int(second)) * long(1e6)
return time(int(hour), int(minute), int(second),
int(microsecond.to_integral()), tzinfo)
raise ISO8601Error('Unrecognised ISO 8601 time format: %r' % timestring)
def time_isoformat(ttime, format=TIME_EXT_COMPLETE + TZ_EXT):
'''
Format time strings.
This method is just a wrapper around isodate.isostrf.strftime and uses
Time-Extended-Complete with extended time zone as default format.
'''
return strftime(ttime, format)

112
src/isodate/isotzinfo.py Normal file
View File

@ -0,0 +1,112 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module provides an ISO 8601:2004 time zone info parser.
It offers a function to parse the time zone offset as specified by ISO 8601.
'''
import re
from isodate.isoerror import ISO8601Error
from isodate.tzinfo import UTC, FixedOffset, ZERO
TZ_REGEX = r"(?P<tzname>(Z|(?P<tzsign>[+-])"\
r"(?P<tzhour>[0-9]{2})(:(?P<tzmin>[0-9]{2}))?)?)"
TZ_RE = re.compile(TZ_REGEX)
def build_tzinfo(tzname, tzsign='+', tzhour=0, tzmin=0):
'''
create a tzinfo instance according to given parameters.
tzname:
'Z' ... return UTC
'' | None ... return None
other ... return FixedOffset
'''
if tzname is None or tzname == '':
return None
if tzname == 'Z':
return UTC
tzsign = ((tzsign == '-') and -1) or 1
return FixedOffset(tzsign * tzhour, tzsign * tzmin, tzname)
def parse_tzinfo(tzstring):
'''
Parses ISO 8601 time zone designators to tzinfo objecs.
A time zone designator can be in the following format:
no designator indicates local time zone
Z UTC
+-hhmm basic hours and minutes
+-hh:mm extended hours and minutes
+-hh hours
'''
match = TZ_RE.match(tzstring)
if match:
groups = match.groupdict()
return build_tzinfo(groups['tzname'], groups['tzsign'],
int(groups['tzhour'] or 0),
int(groups['tzmin'] or 0))
raise ISO8601Error('%s not a valid time zone info' % tzstring)
def tz_isoformat(dt, format='%Z'):
'''
return time zone offset ISO 8601 formatted.
The various ISO formats can be chosen with the format parameter.
if tzinfo is None returns ''
if tzinfo is UTC returns 'Z'
else the offset is rendered to the given format.
format:
%h ... +-HH
%z ... +-HHMM
%Z ... +-HH:MM
'''
tzinfo = dt.tzinfo
if (tzinfo is None) or (tzinfo.utcoffset(dt) is None):
return ''
if tzinfo.utcoffset(dt) == ZERO and tzinfo.dst(dt) == ZERO:
return 'Z'
tdelta = tzinfo.utcoffset(dt)
seconds = tdelta.days * 24 * 60 * 60 + tdelta.seconds
sign = ((seconds < 0) and '-') or '+'
seconds = abs(seconds)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
if hours > 99:
raise OverflowError('can not handle differences > 99 hours')
if format == '%Z':
return '%s%02d:%02d' % (sign, hours, minutes)
elif format == '%z':
return '%s%02d%02d' % (sign, hours, minutes)
elif format == '%h':
return '%s%02d' % (sign, hours)
raise ValueError('unknown format string "%s"' % format)

View File

@ -0,0 +1,50 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Collect all test suites into one TestSuite instance.
'''
import unittest
from isodate.tests import (test_date, test_time, test_datetime, test_duration,
test_strf, test_pickle)
def test_suite():
'''
Return a new TestSuite instance consisting of all available TestSuites.
'''
return unittest.TestSuite([
test_date.test_suite(),
test_time.test_suite(),
test_datetime.test_suite(),
test_duration.test_suite(),
test_strf.test_suite(),
test_pickle.test_suite(),
])
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,129 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isodate module.
'''
import unittest
from datetime import date
from isodate import parse_date, ISO8601Error, date_isoformat
from isodate import DATE_CENTURY, DATE_YEAR, DATE_MONTH
from isodate import DATE_EXT_COMPLETE, DATE_BAS_COMPLETE
from isodate import DATE_BAS_ORD_COMPLETE, DATE_EXT_ORD_COMPLETE
from isodate import DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE
from isodate import DATE_EXT_WEEK, DATE_EXT_WEEK_COMPLETE
# the following list contains tuples of ISO date strings and the expected
# result from the parse_date method. A result of None means an ISO8601Error
# is expected. The test cases are grouped into dates with 4 digit years
# and 6 digit years.
TEST_CASES = {4: [('19', date(1901, 1, 1), DATE_CENTURY),
('1985', date(1985, 1, 1), DATE_YEAR),
('1985-04', date(1985, 4, 1), DATE_MONTH),
('1985-04-12', date(1985, 4, 12), DATE_EXT_COMPLETE),
('19850412', date(1985, 4, 12), DATE_BAS_COMPLETE),
('1985102', date(1985, 4, 12), DATE_BAS_ORD_COMPLETE),
('1985-102', date(1985, 4, 12), DATE_EXT_ORD_COMPLETE),
('1985W155', date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE),
('1985-W15-5', date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE),
('1985W15', date(1985, 4, 8), DATE_BAS_WEEK),
('1985-W15', date(1985, 4, 8), DATE_EXT_WEEK),
('1989-W15', date(1989, 4, 10), DATE_EXT_WEEK),
('1989-W15-5', date(1989, 4, 14), DATE_EXT_WEEK_COMPLETE),
('1-W1-1', None, DATE_BAS_WEEK_COMPLETE)],
6: [('+0019', date(1901, 1, 1), DATE_CENTURY),
('+001985', date(1985, 1, 1), DATE_YEAR),
('+001985-04', date(1985, 4, 1), DATE_MONTH),
('+001985-04-12', date(1985, 4, 12), DATE_EXT_COMPLETE),
('+0019850412', date(1985, 4, 12), DATE_BAS_COMPLETE),
('+001985102', date(1985, 4, 12), DATE_BAS_ORD_COMPLETE),
('+001985-102', date(1985, 4, 12), DATE_EXT_ORD_COMPLETE),
('+001985W155', date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE),
('+001985-W15-5', date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE),
('+001985W15', date(1985, 4, 8), DATE_BAS_WEEK),
('+001985-W15', date(1985, 4, 8), DATE_EXT_WEEK)]}
def create_testcase(yeardigits, datestring, expectation, format):
'''
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
TEST_CASES list, so that a failed test won't stop other tests.
'''
class TestDate(unittest.TestCase):
'''
A test case template to parse an ISO date string into a date
object.
'''
def test_parse(self):
'''
Parse an ISO date string and compare it to the expected value.
'''
if expectation is None:
self.assertRaises(ISO8601Error, parse_date, datestring,
yeardigits)
else:
result = parse_date(datestring, yeardigits)
self.assertEqual(result, expectation)
def test_format(self):
'''
Take date object and create ISO string from it.
This is the reverse test to test_parse.
'''
if expectation is None:
self.assertRaises(AttributeError,
date_isoformat, expectation, format,
yeardigits)
else:
self.assertEqual(date_isoformat(expectation, format,
yeardigits),
datestring)
return unittest.TestLoader().loadTestsFromTestCase(TestDate)
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
for yeardigits, tests in TEST_CASES.items():
for datestring, expectation, format in tests:
suite.addTest(create_testcase(yeardigits, datestring,
expectation, format))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,146 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isodatetime module.
'''
import unittest
from datetime import datetime
from isodate import parse_datetime, UTC, FixedOffset, datetime_isoformat
from isodate import ISO8601Error
from isodate import DATE_BAS_COMPLETE, TIME_BAS_MINUTE, TIME_BAS_COMPLETE
from isodate import DATE_EXT_COMPLETE, TIME_EXT_MINUTE, TIME_EXT_COMPLETE
from isodate import TZ_BAS, TZ_EXT, TZ_HOUR
from isodate import DATE_BAS_ORD_COMPLETE, DATE_EXT_ORD_COMPLETE
from isodate import DATE_BAS_WEEK_COMPLETE, DATE_EXT_WEEK_COMPLETE
# the following list contains tuples of ISO datetime strings and the expected
# result from the parse_datetime method. A result of None means an ISO8601Error
# is expected.
TEST_CASES = [('19850412T1015', datetime(1985, 4, 12, 10, 15),
DATE_BAS_COMPLETE + 'T' + TIME_BAS_MINUTE,
'19850412T1015'),
('1985-04-12T10:15', datetime(1985, 4, 12, 10, 15),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_MINUTE,
'1985-04-12T10:15'),
('1985102T1015Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC),
DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_MINUTE + TZ_BAS,
'1985102T1015Z'),
('1985-102T10:15Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC),
DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_MINUTE + TZ_EXT,
'1985-102T10:15Z'),
('1985W155T1015+0400', datetime(1985, 4, 12, 10, 15,
tzinfo=FixedOffset(4, 0,
'+0400')),
DATE_BAS_WEEK_COMPLETE + 'T' + TIME_BAS_MINUTE + TZ_BAS,
'1985W155T1015+0400'),
('1985-W15-5T10:15+04', datetime(1985, 4, 12, 10, 15,
tzinfo=FixedOffset(4, 0,
'+0400'),),
DATE_EXT_WEEK_COMPLETE + 'T' + TIME_EXT_MINUTE + TZ_HOUR,
'1985-W15-5T10:15+04'),
('20110410T101225.123000Z',
datetime(2011, 4, 10, 10, 12, 25, 123000, tzinfo=UTC),
DATE_BAS_COMPLETE + 'T' + TIME_BAS_COMPLETE + ".%f" + TZ_BAS,
'20110410T101225.123000Z'),
('2012-10-12T08:29:46.069178Z',
datetime(2012, 10, 12, 8, 29, 46, 69178, tzinfo=UTC),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS,
'2012-10-12T08:29:46.069178Z'),
('2012-10-12T08:29:46.691780Z',
datetime(2012, 10, 12, 8, 29, 46, 691780, tzinfo=UTC),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS,
'2012-10-12T08:29:46.691780Z'),
('2012-10-30T08:55:22.1234567Z',
datetime(2012, 10, 30, 8, 55, 22, 123457, tzinfo=UTC),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS,
'2012-10-30T08:55:22.123457Z'),
('2012-10-30T08:55:22.1234561Z',
datetime(2012, 10, 30, 8, 55, 22, 123456, tzinfo=UTC),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS,
'2012-10-30T08:55:22.123456Z'),
('2014-08-18 14:55:22.123456Z', None,
DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS,
'2014-08-18T14:55:22.123456Z'),
]
def create_testcase(datetimestring, expectation, format, output):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestDateTime(unittest.TestCase):
'''
A test case template to parse an ISO datetime string into a
datetime object.
'''
def test_parse(self):
'''
Parse an ISO datetime string and compare it to the expected value.
'''
if expectation is None:
self.assertRaises(ISO8601Error, parse_datetime, datetimestring)
else:
self.assertEqual(parse_datetime(datetimestring), expectation)
def test_format(self):
'''
Take datetime object and create ISO string from it.
This is the reverse test to test_parse.
'''
if expectation is None:
self.assertRaises(AttributeError,
datetime_isoformat, expectation, format)
else:
self.assertEqual(datetime_isoformat(expectation, format),
output)
return unittest.TestLoader().loadTestsFromTestCase(TestDateTime)
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
for datetimestring, expectation, format, output in TEST_CASES:
suite.addTest(create_testcase(datetimestring, expectation,
format, output))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,601 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isoduration module.
'''
import unittest
import operator
from datetime import timedelta, date, datetime
from isodate import Duration, parse_duration, ISO8601Error
from isodate import D_DEFAULT, D_WEEK, D_ALT_EXT, duration_isoformat
# the following list contains tuples of ISO duration strings and the expected
# result from the parse_duration method. A result of None means an ISO8601Error
# is expected.
PARSE_TEST_CASES = {'P18Y9M4DT11H9M8S': (Duration(4, 8, 0, 0, 9, 11, 0, 9, 18),
D_DEFAULT, None),
'P2W': (timedelta(weeks=2), D_WEEK, None),
'P3Y6M4DT12H30M5S': (Duration(4, 5, 0, 0, 30, 12, 0, 6, 3),
D_DEFAULT, None),
'P23DT23H': (timedelta(hours=23, days=23),
D_DEFAULT, None),
'P4Y': (Duration(years=4), D_DEFAULT, None),
'P1M': (Duration(months=1), D_DEFAULT, None),
'PT1M': (timedelta(minutes=1), D_DEFAULT, None),
'P0.5Y': (Duration(years=0.5), D_DEFAULT, None),
'PT36H': (timedelta(hours=36), D_DEFAULT, 'P1DT12H'),
'P1DT12H': (timedelta(days=1, hours=12), D_DEFAULT, None),
'+P11D': (timedelta(days=11), D_DEFAULT, 'P11D'),
'-P2W': (timedelta(weeks=-2), D_WEEK, None),
'-P2.2W': (timedelta(weeks=-2.2), D_DEFAULT,
'-P15DT9H36M'),
'P1DT2H3M4S': (timedelta(days=1, hours=2, minutes=3,
seconds=4), D_DEFAULT, None),
'P1DT2H3M': (timedelta(days=1, hours=2, minutes=3),
D_DEFAULT, None),
'P1DT2H': (timedelta(days=1, hours=2), D_DEFAULT, None),
'PT2H': (timedelta(hours=2), D_DEFAULT, None),
'PT2.3H': (timedelta(hours=2.3), D_DEFAULT, 'PT2H18M'),
'PT2H3M4S': (timedelta(hours=2, minutes=3, seconds=4),
D_DEFAULT, None),
'PT3M4S': (timedelta(minutes=3, seconds=4), D_DEFAULT,
None),
'PT22S': (timedelta(seconds=22), D_DEFAULT, None),
'PT22.22S': (timedelta(seconds=22.22), 'PT%S.%fS',
'PT22.220000S'),
'-P2Y': (Duration(years=-2), D_DEFAULT, None),
'-P3Y6M4DT12H30M5S': (Duration(-4, -5, 0, 0, -30, -12, 0,
-6, -3), D_DEFAULT, None),
'-P1DT2H3M4S': (timedelta(days=-1, hours=-2, minutes=-3,
seconds=-4), D_DEFAULT, None),
# alternative format
'P0018-09-04T11:09:08': (Duration(4, 8, 0, 0, 9, 11, 0, 9,
18), D_ALT_EXT, None),
# 'PT000022.22': timedelta(seconds=22.22),
}
# d1 d2 '+', '-', '>'
# A list of test cases to test addition and subtraction between datetime and
# Duration objects.
# each tuple contains 2 duration strings, and a result string for addition and
# one for subtraction. The last value says, if the first duration is greater
# than the second.
MATH_TEST_CASES = (('P5Y7M1DT9H45M16.72S', 'PT27M24.68S',
'P5Y7M1DT10H12M41.4S', 'P5Y7M1DT9H17M52.04S', None),
('PT28M12.73S', 'PT56M29.92S',
'PT1H24M42.65S', '-PT28M17.19S', False),
('P3Y7M23DT5H25M0.33S', 'PT1H1.95S',
'P3Y7M23DT6H25M2.28S', 'P3Y7M23DT4H24M58.38S', None),
('PT1H1.95S', 'P3Y7M23DT5H25M0.33S',
'P3Y7M23DT6H25M2.28S', '-P3Y7M23DT4H24M58.38S', None),
('P1332DT55M0.33S', 'PT1H1.95S',
'P1332DT1H55M2.28S', 'P1331DT23H54M58.38S', True),
('PT1H1.95S', 'P1332DT55M0.33S',
'P1332DT1H55M2.28S', '-P1331DT23H54M58.38S', False))
# A list of test cases to test addition and subtraction of date/datetime
# and Duration objects. They are tested against the results of an
# equal long timedelta duration.
DATE_TEST_CASES = ((date(2008, 2, 29),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(date(2008, 1, 31),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2008, 2, 29),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2008, 1, 31),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2008, 4, 21),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2008, 5, 5),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2000, 1, 1),
timedelta(hours=-33),
Duration(hours=-33)),
(datetime(2008, 5, 5),
Duration(years=1, months=1, days=10, hours=12,
minutes=20),
Duration(months=13, days=10, hours=12, minutes=20)),
(datetime(2000, 3, 30),
Duration(years=1, months=1, days=10, hours=12,
minutes=20),
Duration(months=13, days=10, hours=12, minutes=20)),
)
# A list of test cases of additon of date/datetime and Duration. The results
# are compared against a given expected result.
DATE_CALC_TEST_CASES = (
(date(2000, 2, 1),
Duration(years=1, months=1),
date(2001, 3, 1)),
(date(2000, 2, 29),
Duration(years=1, months=1),
date(2001, 3, 29)),
(date(2000, 2, 29),
Duration(years=1),
date(2001, 2, 28)),
(date(1996, 2, 29),
Duration(years=4),
date(2000, 2, 29)),
(date(2096, 2, 29),
Duration(years=4),
date(2100, 2, 28)),
(date(2000, 2, 1),
Duration(years=-1, months=-1),
date(1999, 1, 1)),
(date(2000, 2, 29),
Duration(years=-1, months=-1),
date(1999, 1, 29)),
(date(2000, 2, 1),
Duration(years=1, months=1, days=1),
date(2001, 3, 2)),
(date(2000, 2, 29),
Duration(years=1, months=1, days=1),
date(2001, 3, 30)),
(date(2000, 2, 29),
Duration(years=1, days=1),
date(2001, 3, 1)),
(date(1996, 2, 29),
Duration(years=4, days=1),
date(2000, 3, 1)),
(date(2096, 2, 29),
Duration(years=4, days=1),
date(2100, 3, 1)),
(date(2000, 2, 1),
Duration(years=-1, months=-1, days=-1),
date(1998, 12, 31)),
(date(2000, 2, 29),
Duration(years=-1, months=-1, days=-1),
date(1999, 1, 28)),
(date(2001, 4, 1),
Duration(years=-1, months=-1, days=-1),
date(2000, 2, 29)),
(date(2000, 4, 1),
Duration(years=-1, months=-1, days=-1),
date(1999, 2, 28)),
(Duration(years=1, months=2),
Duration(years=0, months=0, days=1),
Duration(years=1, months=2, days=1)),
(Duration(years=-1, months=-1, days=-1),
date(2000, 4, 1),
date(1999, 2, 28)),
(Duration(years=1, months=1, weeks=5),
date(2000, 1, 30),
date(2001, 4, 4)),
(parse_duration("P1Y1M5W"),
date(2000, 1, 30),
date(2001, 4, 4)),
(parse_duration("P0.5Y"),
date(2000, 1, 30),
None),
(Duration(years=1, months=1, hours=3),
datetime(2000, 1, 30, 12, 15, 00),
datetime(2001, 2, 28, 15, 15, 00)),
(parse_duration("P1Y1MT3H"),
datetime(2000, 1, 30, 12, 15, 00),
datetime(2001, 2, 28, 15, 15, 00)),
(Duration(years=1, months=2),
timedelta(days=1),
Duration(years=1, months=2, days=1)),
(timedelta(days=1),
Duration(years=1, months=2),
Duration(years=1, months=2, days=1)),
(datetime(2008, 1, 1, 0, 2),
Duration(months=1),
datetime(2008, 2, 1, 0, 2)),
(datetime.strptime("200802", "%Y%M"),
parse_duration("P1M"),
datetime(2008, 2, 1, 0, 2)),
(datetime(2008, 2, 1),
Duration(months=1),
datetime(2008, 3, 1)),
(datetime.strptime("200802", "%Y%m"),
parse_duration("P1M"),
datetime(2008, 3, 1)),
# (date(2000, 1, 1),
# Duration(years=1.5),
# date(2001, 6, 1)),
# (date(2000, 1, 1),
# Duration(years=1, months=1.5),
# date(2001, 2, 14)),
)
# A list of test cases of multiplications of durations
# are compared against a given expected result.
DATE_MUL_TEST_CASES = (
(Duration(years=1, months=1),
3,
Duration(years=3, months=3)),
(Duration(years=1, months=1),
-3,
Duration(years=-3, months=-3)),
(3,
Duration(years=1, months=1),
Duration(years=3, months=3)),
(-3,
Duration(years=1, months=1),
Duration(years=-3, months=-3)),
(5,
Duration(years=2, minutes=40),
Duration(years=10, hours=3, minutes=20)),
(-5,
Duration(years=2, minutes=40),
Duration(years=-10, hours=-3, minutes=-20)),
(7,
Duration(years=1, months=2, weeks=40),
Duration(years=8, months=2, weeks=280)))
class DurationTest(unittest.TestCase):
'''
This class tests various other aspects of the isoduration module,
which are not covered with the test cases listed above.
'''
def test_associative(self):
'''
Adding 2 durations to a date is not associative.
'''
days1 = Duration(days=1)
months1 = Duration(months=1)
start = date(2000, 3, 30)
res1 = start + days1 + months1
res2 = start + months1 + days1
self.assertNotEqual(res1, res2)
def test_typeerror(self):
'''
Test if TypError is raised with certain parameters.
'''
self.assertRaises(TypeError, parse_duration, date(2000, 1, 1))
self.assertRaises(TypeError, operator.sub, Duration(years=1),
date(2000, 1, 1))
self.assertRaises(TypeError, operator.sub, 'raise exc',
Duration(years=1))
self.assertRaises(TypeError, operator.add,
Duration(years=1, months=1, weeks=5),
'raise exception')
self.assertRaises(TypeError, operator.add, 'raise exception',
Duration(years=1, months=1, weeks=5))
self.assertRaises(TypeError, operator.mul,
Duration(years=1, months=1, weeks=5),
'raise exception')
self.assertRaises(TypeError, operator.mul, 'raise exception',
Duration(years=1, months=1, weeks=5))
self.assertRaises(TypeError, operator.mul,
Duration(years=1, months=1, weeks=5),
3.14)
self.assertRaises(TypeError, operator.mul, 3.14,
Duration(years=1, months=1, weeks=5))
def test_parseerror(self):
'''
Test for unparseable duration string.
'''
self.assertRaises(ISO8601Error, parse_duration, 'T10:10:10')
def test_repr(self):
'''
Test __repr__ and __str__ for Duration objects.
'''
dur = Duration(10, 10, years=10, months=10)
self.assertEqual('10 years, 10 months, 10 days, 0:00:10', str(dur))
self.assertEqual('isodate.duration.Duration(10, 10, 0,'
' years=10, months=10)', repr(dur))
def test_hash(self):
'''
Test __hash__ for Duration objects.
'''
dur1 = Duration(10, 10, years=10, months=10)
dur2 = Duration(9, 9, years=9, months=9)
dur3 = Duration(10, 10, years=10, months=10)
self.assertNotEqual(hash(dur1), hash(dur2))
self.assertNotEqual(id(dur1), id(dur2))
self.assertEqual(hash(dur1), hash(dur3))
self.assertNotEqual(id(dur1), id(dur3))
durSet = set()
durSet.add(dur1)
durSet.add(dur2)
durSet.add(dur3)
self.assertEqual(len(durSet), 2)
def test_neg(self):
'''
Test __neg__ for Duration objects.
'''
self.assertEqual(-Duration(0), Duration(0))
self.assertEqual(-Duration(years=1, months=1),
Duration(years=-1, months=-1))
self.assertEqual(-Duration(years=1, months=1), Duration(months=-13))
self.assertNotEqual(-Duration(years=1), timedelta(days=-365))
self.assertNotEqual(-timedelta(days=365), Duration(years=-1))
# FIXME: this test fails in python 3... it seems like python3
# treats a == b the same b == a
# self.assertNotEqual(-timedelta(days=10), -Duration(days=10))
def test_format(self):
'''
Test various other strftime combinations.
'''
self.assertEqual(duration_isoformat(Duration(0)), 'P0D')
self.assertEqual(duration_isoformat(-Duration(0)), 'P0D')
self.assertEqual(duration_isoformat(Duration(seconds=10)), 'PT10S')
self.assertEqual(duration_isoformat(Duration(years=-1, months=-1)),
'-P1Y1M')
self.assertEqual(duration_isoformat(-Duration(years=1, months=1)),
'-P1Y1M')
self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)),
'P1Y1M')
self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)),
'P1Y1M')
dur = Duration(years=3, months=7, days=23, hours=5, minutes=25,
milliseconds=330)
self.assertEqual(duration_isoformat(dur), 'P3Y7M23DT5H25M0.33S')
self.assertEqual(duration_isoformat(-dur), '-P3Y7M23DT5H25M0.33S')
def test_equal(self):
'''
Test __eq__ and __ne__ methods.
'''
self.assertEqual(Duration(years=1, months=1),
Duration(years=1, months=1))
self.assertEqual(Duration(years=1, months=1), Duration(months=13))
self.assertNotEqual(Duration(years=1, months=2),
Duration(years=1, months=1))
self.assertNotEqual(Duration(years=1, months=1), Duration(months=14))
self.assertNotEqual(Duration(years=1), timedelta(days=365))
self.assertFalse(Duration(years=1, months=1) !=
Duration(years=1, months=1))
self.assertFalse(Duration(years=1, months=1) != Duration(months=13))
self.assertTrue(Duration(years=1, months=2) !=
Duration(years=1, months=1))
self.assertTrue(Duration(years=1, months=1) != Duration(months=14))
self.assertTrue(Duration(years=1) != timedelta(days=365))
self.assertEqual(Duration(days=1), timedelta(days=1))
# FIXME: this test fails in python 3... it seems like python3
# treats a != b the same b != a
# self.assertNotEqual(timedelta(days=1), Duration(days=1))
def test_totimedelta(self):
'''
Test conversion form Duration to timedelta.
'''
dur = Duration(years=1, months=2, days=10)
self.assertEqual(dur.totimedelta(datetime(1998, 2, 25)),
timedelta(434))
# leap year has one day more in february
self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)),
timedelta(435))
dur = Duration(months=2)
# march is longer than february, but april is shorter than
# march (cause only one day difference compared to 2)
self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)), timedelta(60))
self.assertEqual(dur.totimedelta(datetime(2001, 2, 25)), timedelta(59))
self.assertEqual(dur.totimedelta(datetime(2001, 3, 25)), timedelta(61))
def create_parsetestcase(durationstring, expectation, format, altstr):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
PARSE_TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestParseDuration(unittest.TestCase):
'''
A test case template to parse an ISO duration string into a
timedelta or Duration object.
'''
def test_parse(self):
'''
Parse an ISO duration string and compare it to the expected value.
'''
result = parse_duration(durationstring)
self.assertEqual(result, expectation)
def test_format(self):
'''
Take duration/timedelta object and create ISO string from it.
This is the reverse test to test_parse.
'''
if altstr:
self.assertEqual(duration_isoformat(expectation, format),
altstr)
else:
# if durationstring == '-P2W':
# import pdb; pdb.set_trace()
self.assertEqual(duration_isoformat(expectation, format),
durationstring)
return unittest.TestLoader().loadTestsFromTestCase(TestParseDuration)
def create_mathtestcase(dur1, dur2, resadd, ressub, resge):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
MATH_TEST_CASES list, so that a failed test won't stop other tests.
"""
dur1 = parse_duration(dur1)
dur2 = parse_duration(dur2)
resadd = parse_duration(resadd)
ressub = parse_duration(ressub)
class TestMathDuration(unittest.TestCase):
'''
A test case template test addition, subtraction and >
operators for Duration objects.
'''
def test_add(self):
'''
Test operator + (__add__, __radd__)
'''
self.assertEqual(dur1 + dur2, resadd)
def test_sub(self):
'''
Test operator - (__sub__, __rsub__)
'''
self.assertEqual(dur1 - dur2, ressub)
def test_ge(self):
'''
Test operator > and <
'''
def dogetest():
''' Test greater than.'''
return dur1 > dur2
def doletest():
''' Test less than.'''
return dur1 < dur2
if resge is None:
self.assertRaises(TypeError, dogetest)
self.assertRaises(TypeError, doletest)
else:
self.assertEqual(dogetest(), resge)
self.assertEqual(doletest(), not resge)
return unittest.TestLoader().loadTestsFromTestCase(TestMathDuration)
def create_datetestcase(start, tdelta, duration):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
DATE_TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestDateCalc(unittest.TestCase):
'''
A test case template test addition, subtraction
operators for Duration objects.
'''
def test_add(self):
'''
Test operator +.
'''
self.assertEqual(start + tdelta, start + duration)
def test_sub(self):
'''
Test operator -.
'''
self.assertEqual(start - tdelta, start - duration)
return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc)
def create_datecalctestcase(start, duration, expectation):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
DATE_CALC_TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestDateCalc(unittest.TestCase):
'''
A test case template test addition operators for Duration objects.
'''
def test_calc(self):
'''
Test operator +.
'''
if expectation is None:
self.assertRaises(ValueError, operator.add, start, duration)
else:
self.assertEqual(start + duration, expectation)
return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc)
def create_datemultestcase(operand1, operand2, expectation):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
DATE_CALC_TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestDateMul(unittest.TestCase):
'''
A test case template test addition operators for Duration objects.
'''
def test_mul(self):
'''
Test operator *.
'''
self.assertEqual(operand1 * operand2, expectation)
return unittest.TestLoader().loadTestsFromTestCase(TestDateMul)
def test_suite():
'''
Return a test suite containing all test defined above.
'''
suite = unittest.TestSuite()
for durationstring, (expectation, format,
altstr) in PARSE_TEST_CASES.items():
suite.addTest(create_parsetestcase(durationstring, expectation,
format, altstr))
for testdata in MATH_TEST_CASES:
suite.addTest(create_mathtestcase(*testdata))
for testdata in DATE_TEST_CASES:
suite.addTest(create_datetestcase(*testdata))
for testdata in DATE_CALC_TEST_CASES:
suite.addTest(create_datecalctestcase(*testdata))
for testdata in DATE_MUL_TEST_CASES:
suite.addTest(create_datemultestcase(*testdata))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DurationTest))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,54 @@
import unittest
import cPickle as pickle
import isodate
class TestPickle(unittest.TestCase):
'''
A test case template to parse an ISO datetime string into a
datetime object.
'''
def test_pickle_datetime(self):
'''
Parse an ISO datetime string and compare it to the expected value.
'''
dti = isodate.parse_datetime('2012-10-26T09:33+00:00')
for proto in range(0, pickle.HIGHEST_PROTOCOL + 1):
pikl = pickle.dumps(dti, proto)
self.assertEqual(dti, pickle.loads(pikl),
"pickle proto %d failed" % proto)
def test_pickle_duration(self):
'''
Pickle / unpickle duration objects.
'''
from isodate.duration import Duration
dur = Duration()
failed = []
for proto in range(0, pickle.HIGHEST_PROTOCOL + 1):
try:
pikl = pickle.dumps(dur, proto)
if dur != pickle.loads(pikl):
raise Exception("not equal")
except Exception, e:
failed.append("pickle proto %d failed (%s)" % (proto, repr(e)))
self.assertEqual(len(failed), 0, "pickle protos failed: %s" %
str(failed))
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestPickle))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,135 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isodate module.
'''
import unittest
import time
from datetime import datetime, timedelta
from isodate import strftime
from isodate import LOCAL
from isodate import DT_EXT_COMPLETE
from isodate import tzinfo
TEST_CASES = ((datetime(2012, 12, 25, 13, 30, 0, 0, LOCAL), DT_EXT_COMPLETE,
"2012-12-25T13:30:00+10:00"),
# DST ON
(datetime(1999, 12, 25, 13, 30, 0, 0, LOCAL), DT_EXT_COMPLETE,
"1999-12-25T13:30:00+11:00"),
# microseconds
(datetime(2012, 10, 12, 8, 29, 46, 69178),
"%Y-%m-%dT%H:%M:%S.%f",
"2012-10-12T08:29:46.069178"),
(datetime(2012, 10, 12, 8, 29, 46, 691780),
"%Y-%m-%dT%H:%M:%S.%f",
"2012-10-12T08:29:46.691780"),
)
def create_testcase(dt, format, expectation):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestDate(unittest.TestCase):
'''
A test case template to test ISO date formatting.
'''
# local time zone mock function
def localtime_mock(self, secs):
"""
mock time.localtime so that it always returns a time_struct with
tm_idst=1
"""
tt = self.ORIG['localtime'](secs)
# befor 2000 everything is dst, after 2000 no dst.
if tt.tm_year < 2000:
dst = 1
else:
dst = 0
tt = (tt.tm_year, tt.tm_mon, tt.tm_mday,
tt.tm_hour, tt.tm_min, tt.tm_sec,
tt.tm_wday, tt.tm_yday, dst)
return time.struct_time(tt)
def setUp(self):
self.ORIG = {}
self.ORIG['STDOFFSET'] = tzinfo.STDOFFSET
self.ORIG['DSTOFFSET'] = tzinfo.DSTOFFSET
self.ORIG['DSTDIFF'] = tzinfo.DSTDIFF
self.ORIG['localtime'] = time.localtime
# ovveride all saved values with fixtures.
# calculate LOCAL TZ offset, so that this test runs in
# every time zone
tzinfo.STDOFFSET = timedelta(seconds=36000) # assume LOC = +10:00
tzinfo.DSTOFFSET = timedelta(seconds=39600) # assume DST = +11:00
tzinfo.DSTDIFF = tzinfo.DSTOFFSET - tzinfo.STDOFFSET
time.localtime = self.localtime_mock
def tearDown(self):
# restore test fixtures
tzinfo.STDOFFSET = self.ORIG['STDOFFSET']
tzinfo.DSTOFFSET = self.ORIG['DSTOFFSET']
tzinfo.DSTDIFF = self.ORIG['DSTDIFF']
time.localtime = self.ORIG['localtime']
def test_format(self):
'''
Take date object and create ISO string from it.
This is the reverse test to test_parse.
'''
if expectation is None:
self.assertRaises(AttributeError,
strftime(dt, format))
else:
self.assertEqual(strftime(dt, format),
expectation)
return unittest.TestLoader().loadTestsFromTestCase(TestDate)
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
for dt, format, expectation in TEST_CASES:
suite.addTest(create_testcase(dt, format, expectation))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,143 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isotime module.
'''
import unittest
from datetime import time
from isodate import parse_time, UTC, FixedOffset, ISO8601Error, time_isoformat
from isodate import TIME_BAS_COMPLETE, TIME_BAS_MINUTE
from isodate import TIME_EXT_COMPLETE, TIME_EXT_MINUTE
from isodate import TIME_HOUR
from isodate import TZ_BAS, TZ_EXT, TZ_HOUR
# the following list contains tuples of ISO time strings and the expected
# result from the parse_time method. A result of None means an ISO8601Error
# is expected.
TEST_CASES = [('232050', time(23, 20, 50), TIME_BAS_COMPLETE + TZ_BAS),
('23:20:50', time(23, 20, 50), TIME_EXT_COMPLETE + TZ_EXT),
('2320', time(23, 20), TIME_BAS_MINUTE),
('23:20', time(23, 20), TIME_EXT_MINUTE),
('23', time(23), TIME_HOUR),
('232050,5', time(23, 20, 50, 500000), None),
('23:20:50.5', time(23, 20, 50, 500000), None),
# test precision
('15:33:42.123456', time(15, 33, 42, 123456), None),
('15:33:42.1234564', time(15, 33, 42, 123456), None),
('15:33:42.1234557', time(15, 33, 42, 123456), None),
('2320,8', time(23, 20, 48), None),
('23:20,8', time(23, 20, 48), None),
('23,3', time(23, 18), None),
('232030Z', time(23, 20, 30, tzinfo=UTC),
TIME_BAS_COMPLETE + TZ_BAS),
('2320Z', time(23, 20, tzinfo=UTC), TIME_BAS_MINUTE + TZ_BAS),
('23Z', time(23, tzinfo=UTC), TIME_HOUR + TZ_BAS),
('23:20:30Z', time(23, 20, 30, tzinfo=UTC),
TIME_EXT_COMPLETE + TZ_EXT),
('23:20Z', time(23, 20, tzinfo=UTC), TIME_EXT_MINUTE + TZ_EXT),
('152746+0100', time(15, 27, 46,
tzinfo=FixedOffset(1, 0, '+0100')), TIME_BAS_COMPLETE + TZ_BAS),
('152746-0500', time(15, 27, 46,
tzinfo=FixedOffset(-5, 0, '-0500')),
TIME_BAS_COMPLETE + TZ_BAS),
('152746+01', time(15, 27, 46,
tzinfo=FixedOffset(1, 0, '+01:00')),
TIME_BAS_COMPLETE + TZ_HOUR),
('152746-05', time(15, 27, 46,
tzinfo=FixedOffset(-5, -0, '-05:00')),
TIME_BAS_COMPLETE + TZ_HOUR),
('15:27:46+01:00', time(15, 27, 46,
tzinfo=FixedOffset(1, 0, '+01:00')),
TIME_EXT_COMPLETE + TZ_EXT),
('15:27:46-05:00', time(15, 27, 46,
tzinfo=FixedOffset(-5, -0, '-05:00')),
TIME_EXT_COMPLETE + TZ_EXT),
('15:27:46+01', time(15, 27, 46,
tzinfo=FixedOffset(1, 0, '+01:00')),
TIME_EXT_COMPLETE + TZ_HOUR),
('15:27:46-05', time(15, 27, 46,
tzinfo=FixedOffset(-5, -0, '-05:00')),
TIME_EXT_COMPLETE + TZ_HOUR),
('1:17:30', None, TIME_EXT_COMPLETE)]
def create_testcase(timestring, expectation, format):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestTime(unittest.TestCase):
'''
A test case template to parse an ISO time string into a time
object.
'''
def test_parse(self):
'''
Parse an ISO time string and compare it to the expected value.
'''
if expectation is None:
self.assertRaises(ISO8601Error, parse_time, timestring)
else:
result = parse_time(timestring)
self.assertEqual(result, expectation)
def test_format(self):
'''
Take time object and create ISO string from it.
This is the reverse test to test_parse.
'''
if expectation is None:
self.assertRaises(AttributeError,
time_isoformat, expectation, format)
elif format is not None:
self.assertEqual(time_isoformat(expectation, format),
timestring)
return unittest.TestLoader().loadTestsFromTestCase(TestTime)
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
for timestring, expectation, format in TEST_CASES:
suite.addTest(create_testcase(timestring, expectation, format))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

142
src/isodate/tzinfo.py Normal file
View File

@ -0,0 +1,142 @@
'''
This module provides some datetime.tzinfo implementations.
All those classes are taken from the Python documentation.
'''
from datetime import timedelta, tzinfo
import time
ZERO = timedelta(0)
# constant for zero time offset.
class Utc(tzinfo):
'''UTC
Universal time coordinated time zone.
'''
def utcoffset(self, dt):
'''
Return offset from UTC in minutes east of UTC, which is ZERO for UTC.
'''
return ZERO
def tzname(self, dt):
'''
Return the time zone name corresponding to the datetime object dt,
as a string.
'''
return "UTC"
def dst(self, dt):
'''
Return the daylight saving time (DST) adjustment, in minutes east
of UTC.
'''
return ZERO
UTC = Utc()
# the default instance for UTC.
class FixedOffset(tzinfo):
'''
A class building tzinfo objects for fixed-offset time zones.
Note that FixedOffset(0, 0, "UTC") or FixedOffset() is a different way to
build a UTC tzinfo object.
'''
def __init__(self, offset_hours=0, offset_minutes=0, name="UTC"):
'''
Initialise an instance with time offset and name.
The time offset should be positive for time zones east of UTC
and negate for time zones west of UTC.
'''
self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes)
self.__name = name
def utcoffset(self, dt):
'''
Return offset from UTC in minutes of UTC.
'''
return self.__offset
def tzname(self, dt):
'''
Return the time zone name corresponding to the datetime object dt, as a
string.
'''
return self.__name
def dst(self, dt):
'''
Return the daylight saving time (DST) adjustment, in minutes east of
UTC.
'''
return ZERO
def __repr__(self):
'''
Return nicely formatted repr string.
'''
return "<FixedOffset %r>" % self.__name
STDOFFSET = timedelta(seconds=-time.timezone)
# locale time zone offset
# calculate local daylight saving offset if any.
if time.daylight:
DSTOFFSET = timedelta(seconds=-time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
# difference between local time zone and local DST time zone
class LocalTimezone(tzinfo):
"""
A class capturing the platform's idea of local time.
"""
def utcoffset(self, dt):
'''
Return offset from UTC in minutes of UTC.
'''
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
def dst(self, dt):
'''
Return daylight saving offset.
'''
if self._isdst(dt):
return DSTDIFF
else:
return ZERO
def tzname(self, dt):
'''
Return the time zone name corresponding to the datetime object dt, as a
string.
'''
return time.tzname[self._isdst(dt)]
def _isdst(self, dt):
'''
Returns true if DST is active for given datetime object dt.
'''
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, -1)
stamp = time.mktime(tt)
tt = time.localtime(stamp)
return tt.tm_isdst > 0
LOCAL = LocalTimezone()
# the default instance for local time zone.