summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIlya Etingof <etingof@gmail.com>2017-07-18 00:36:13 +0200
committerIlya Etingof <etingof@gmail.com>2017-07-18 00:36:13 +0200
commit5a5f186eb9367e6fff602de5cc26300f071f8062 (patch)
treeaf76397a5cf2dcafb18f088d667750daa8f10998
parentd07ef6cb47c48eae585fc20ed17f804bd5dd9965 (diff)
parent3b177a33db98c19c6f2c6b8c90aaae12b6f2c6b0 (diff)
downloadpyasn1-git-5a5f186eb9367e6fff602de5cc26300f071f8062.tar.gz
Merge branch 'master' into open-types-support
-rw-r--r--CHANGES.rst12
-rw-r--r--doc/source/docs/type/useful/generalizedtime.rst22
-rw-r--r--doc/source/docs/type/useful/utctime.rst20
-rw-r--r--pyasn1/__init__.py2
-rw-r--r--pyasn1/type/useful.py144
-rw-r--r--tests/type/__main__.py3
-rw-r--r--tests/type/test_useful.py97
7 files changed, 283 insertions, 17 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index a896433..47cf3cc 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,5 @@
-Revision 0.2.4, released XX-03-2017
+Revision 0.3.1, released XX-07-2017
-----------------------------------
- ANY DEFINED BY clause support implemented
@@ -29,17 +29,19 @@ Revision 0.2.4, released XX-03-2017
- The .getComponent*() methods of constructed ASN.1 types changed
to lazily instantiate underlying type rather than return `None`.
This should simplify its API as initialization like `X[0][1] = 2` becomes
- possible. Beware that this change introduces a deviation from
- original API.
+ possible.
+ WARNING: this change introduces a deviation from the original API.
- The .setComponent*() methods of SetOf/SequenceOf types changed not
to allow uninitialized "holes" inside the sequences of their components.
- They now behave similarly to Python lists. Beware that this change
- introduces a deviation from original API.
+ They now behave similarly to Python lists.
+ WARNING: this change introduces a deviation from the original API.
- Default and optional components en/decoding of Constructed type
refactored towards better efficiency and more control.
- OctetsString and Any decoder optimized to avoid creating ASN.1
objects for chunks of substrate. Instead they now join substrate
chunks together and create ASN.1 object from it just once.
+- The GeneralizedTime and UTCTime types now support to/from Python
+ datetime object conversion.
- Unit tests added for the `compat` sub-package.
- Fixed BitString named bits initialization bug.
- Fixed non-functional tag cache (when running Python 2) at DER decoder.
diff --git a/doc/source/docs/type/useful/generalizedtime.rst b/doc/source/docs/type/useful/generalizedtime.rst
index d6392d1..abf4351 100644
--- a/doc/source/docs/type/useful/generalizedtime.rst
+++ b/doc/source/docs/type/useful/generalizedtime.rst
@@ -7,13 +7,29 @@
------------
.. autoclass:: pyasn1.type.useful.GeneralizedTime(value=NoValue(), tagSet=TagSet(), subtypeSpec=ConstraintsIntersection(), encoding='us-ascii')
- :members: isValue, isSameTypeWith, isSuperTypeOf, tagSet
+ :members: isValue, isSameTypeWith, isSuperTypeOf, tagSet, asDateTime, fromDateTime
.. note::
- The |ASN.1| type models a character string representing date and time in many different
- formats. For example, *20170126120000Z* stands for YYYYMMDDHHMMSSZ.
+ The |ASN.1| type models a character string representing date and time
+ in many different formats.
+
+ Formal syntax for the *GeneralizedTime* value is:
+
+ * **YYYYMMDDhh[mm[ss[(.|,)ffff]]]** standing for a local time, four
+ digits for the year, two for the month, two for the day and two
+ for the hour, followed by two digits for the minutes and two
+ for the seconds if required, then a dot (or a comma), and a
+ number for the fractions of second or
+
+ * a string as above followed by the letter ā€œZā€ (denoting a UTC
+ time) or
+
+ * a string as above followed by a string **(+|-)hh[mm]** denoting
+ time zone offset relative to UTC
+
+ For example, *20170126120000Z* stands for YYYYMMDDHHMMSSZ.
.. automethod:: pyasn1.type.useful.GeneralizedTime.clone(self, value=NoValue(), tagSet=TagSet(), subtypeSpec=ConstraintsIntersection(), encoding='us-ascii')
.. automethod:: pyasn1.type.useful.GeneralizedTime.subtype(self, value=NoValue(), implicitTag=TagSet(), explicitTag=TagSet(),subtypeSpec=ConstraintsIntersection(), encoding='us-ascii')
diff --git a/doc/source/docs/type/useful/utctime.rst b/doc/source/docs/type/useful/utctime.rst
index a7aed6a..2ad86a9 100644
--- a/doc/source/docs/type/useful/utctime.rst
+++ b/doc/source/docs/type/useful/utctime.rst
@@ -7,12 +7,26 @@
------------
.. autoclass:: pyasn1.type.useful.UTCTime(value=NoValue(), tagSet=TagSet(), subtypeSpec=ConstraintsIntersection(), encoding='us-ascii')
- :members: isValue, isSameTypeWith, isSuperTypeOf, tagSet
+ :members: isValue, isSameTypeWith, isSuperTypeOf, tagSet, asDateTime, fromDateTime
.. note::
- The |ASN.1| type models a character string representing date and time. An example
- format is *170126120000Z* which stands for YYMMDDHHMMSSZ.
+ The |ASN.1| type models a character string representing date and time.
+
+ Formal syntax for the *UTCTime* value is:
+
+ * **YYMMDDhhmm[ss]** standing for UTC time, two
+ digits for the year, two for the month, two for the day and two
+ for the hour, followed by two digits for the minutes and two
+ for the seconds if required or
+
+ * a string as above followed by the letter ā€œZā€ (denoting a UTC
+ time) or
+
+ * a string as above followed by a string **(+|-)hhmm** denoting
+ time zone offset relative to UTC
+
+ For example, *170126120000Z* which stands for YYMMDDHHMMSSZ.
.. automethod:: pyasn1.type.useful.UTCTime.clone(self, value=NoValue(), tagSet=TagSet(), subtypeSpec=ConstraintsIntersection(), encoding='us-ascii')
.. automethod:: pyasn1.type.useful.UTCTime.subtype(self, value=NoValue(), implicitTag=TagSet(), explicitTag=TagSet(),subtypeSpec=ConstraintsIntersection(), encoding='us-ascii')
diff --git a/pyasn1/__init__.py b/pyasn1/__init__.py
index 091f6c3..6533ea0 100644
--- a/pyasn1/__init__.py
+++ b/pyasn1/__init__.py
@@ -1,7 +1,7 @@
import sys
# http://www.python.org/dev/peps/pep-0396/
-__version__ = '0.2.4'
+__version__ = '0.3.1'
if sys.version_info[:2] < (2, 4):
raise RuntimeError('PyASN1 requires Python 2.4 or later')
diff --git a/pyasn1/type/useful.py b/pyasn1/type/useful.py
index 0b79a98..0f7c37a 100644
--- a/pyasn1/type/useful.py
+++ b/pyasn1/type/useful.py
@@ -4,7 +4,9 @@
# Copyright (c) 2005-2017, Ilya Etingof <etingof@gmail.com>
# License: http://pyasn1.sf.net/license.html
#
+import datetime
from pyasn1.type import univ, char, tag
+from pyasn1 import error
__all__ = ['ObjectDescriptor', 'GeneralizedTime', 'UTCTime']
@@ -21,19 +23,153 @@ class ObjectDescriptor(char.GraphicString):
)
-class GeneralizedTime(char.VisibleString):
- __doc__ = char.GraphicString.__doc__
+class TimeMixIn(object):
+
+ _yearsDigits = 4
+ _hasSubsecond = False
+ _optionalMinutes = False
+ _shortTZ = False
+
+ class FixedOffset(datetime.tzinfo):
+ """Fixed offset in minutes east from UTC."""
+
+ def __init__(self, offset, name):
+ self.__offset = datetime.timedelta(minutes=offset)
+ self.__name = name
+
+ def utcoffset(self, dt):
+ return self.__offset
+
+ def tzname(self, dt):
+ return self.__name
+
+ def dst(self, dt):
+ return datetime.timedelta(0)
+
+ UTC = FixedOffset(0, 'UTC')
+
+ @property
+ def asDateTime(self):
+ """Create :py:class:`datetime.datetime` object from a |ASN.1| object.
+
+ Returns
+ -------
+ :
+ new instance of :py:class:`datetime.datetime` object
+ """
+ string = str(self)
+ if string.endswith('Z'):
+ tzinfo = TimeMixIn.UTC
+ string = string[:-1]
+
+ elif '-' in string or '+' in string:
+ if '+' in string:
+ string, plusminus, tz = string.partition('+')
+ else:
+ string, plusminus, tz = string.partition('-')
+
+ if self._shortTZ and len(tz) == 2:
+ tz += '00'
+
+ if len(tz) != 4:
+ raise error.PyAsn1Error('malformed time zone offset %s' % tz)
+
+ try:
+ minutes = int(tz[:2]) * 60 + int(tz[2:])
+ if plusminus == '-':
+ minutes *= -1
+
+ except ValueError:
+ raise error.PyAsn1Error('unknown time specification %s' % self)
+
+ tzinfo = TimeMixIn.FixedOffset(minutes, '?')
+
+ else:
+ tzinfo = None
+
+ if '.' in string or ',' in string:
+ if '.' in string:
+ string, _, ms = string.partition('.')
+ else:
+ string, _, ms = string.partition(',')
+
+ try:
+ ms = int(ms) * 10000
+
+ except ValueError:
+ raise error.PyAsn1Error('bad sub-second time specification %s' % self)
+
+ else:
+ ms = 0
+
+ if self._optionalMinutes and len(string) - self._yearsDigits == 6:
+ string += '0000'
+ elif len(string) - self._yearsDigits == 8:
+ string += '00'
+
+ try:
+ dt = datetime.datetime.strptime(string, self._yearsDigits == 4 and '%Y%m%d%H%M%S' or '%y%m%d%H%M%S')
+
+ except ValueError:
+ raise error.PyAsn1Error('malformed datetime format %s' % self)
+
+ return dt.replace(microsecond=ms, tzinfo=tzinfo)
+
+ @classmethod
+ def fromDateTime(cls, dt):
+ """Create |ASN.1| object from a :py:class:`datetime.datetime` object.
+
+ Parameters
+ ----------
+ dt : :py:class:`datetime.datetime` object
+ The `datetime.datetime` object to initialize the |ASN.1| object from
+
+
+ Returns
+ -------
+ :
+ new instance of |ASN.1| value
+ """
+ string = dt.strftime(cls._yearsDigits == 4 and '%Y%m%d%H%M%S' or '%y%m%d%H%M%S')
+ if cls._hasSubsecond:
+ string += '.%d' % (dt.microsecond // 10000)
+
+ if dt.utcoffset():
+ seconds = dt.utcoffset().seconds
+ if seconds < 0:
+ string += '-'
+ else:
+ string += '+'
+ string += '%.2d%.2d' % (seconds // 3600, seconds % 3600)
+ else:
+ string += 'Z'
+
+ return cls(string)
+
+
+class GeneralizedTime(char.VisibleString, TimeMixIn):
+ __doc__ = char.VisibleString.__doc__
#: Default :py:class:`~pyasn1.type.tag.TagSet` object for |ASN.1| objects
tagSet = char.VisibleString.tagSet.tagImplicitly(
tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 24)
)
+ _yearsDigits = 4
+ _hasSubsecond = True
+ _optionalMinutes = True
+ _shortTZ = True
-class UTCTime(char.VisibleString):
- __doc__ = char.GraphicString.__doc__
+
+class UTCTime(char.VisibleString, TimeMixIn):
+ __doc__ = char.VisibleString.__doc__
#: Default :py:class:`~pyasn1.type.tag.TagSet` object for |ASN.1| objects
tagSet = char.VisibleString.tagSet.tagImplicitly(
tag.Tag(tag.tagClassUniversal, tag.tagFormatSimple, 23)
)
+
+ _yearsDigits = 2
+ _hasSubsecond = False
+ _optionalMinutes = False
+ _shortTZ = False
diff --git a/tests/type/__main__.py b/tests/type/__main__.py
index 15036ea..0ad51ce 100644
--- a/tests/type/__main__.py
+++ b/tests/type/__main__.py
@@ -16,7 +16,8 @@ suite = unittest.TestLoader().loadTestsFromNames(
'tests.type.test_namedval.suite',
'tests.type.test_tag.suite',
'tests.type.test_univ.suite',
- 'tests.type.test_char.suite']
+ 'tests.type.test_char.suite',
+ 'tests.type.test_useful.suite']
)
diff --git a/tests/type/test_useful.py b/tests/type/test_useful.py
new file mode 100644
index 0000000..dbd6fe0
--- /dev/null
+++ b/tests/type/test_useful.py
@@ -0,0 +1,97 @@
+#
+# This file is part of pyasn1 software.
+#
+# Copyright (c) 2005-2017, Ilya Etingof <etingof@gmail.com>
+# License: http://pyasn1.sf.net/license.html
+#
+import sys
+import datetime
+from pyasn1.type import useful
+from pyasn1.error import PyAsn1Error
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+
+class FixedOffset(datetime.tzinfo):
+ def __init__(self, offset, name):
+ self.__offset = datetime.timedelta(minutes=offset)
+ self.__name = name
+
+ def utcoffset(self, dt):
+ return self.__offset
+
+ def tzname(self, dt):
+ return self.__name
+
+ def dst(self, dt):
+ return datetime.timedelta(0)
+
+
+UTC = FixedOffset(0, 'UTC')
+UTC2 = FixedOffset(120, 'UTC')
+
+
+class ObjectDescriptorTestCase(unittest.TestCase):
+ pass
+
+
+class GeneralizedTimeTestCase(unittest.TestCase):
+
+ def testFromDateTime(self):
+ assert useful.GeneralizedTime.fromDateTime(datetime.datetime(2017, 7, 11, 0, 1, 2, 30000, tzinfo=UTC)) == '20170711000102.3Z'
+
+ def testToDateTime0(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2) == useful.GeneralizedTime('20170711000102').asDateTime
+
+ def testToDateTime1(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2, tzinfo=UTC) == useful.GeneralizedTime('20170711000102Z').asDateTime
+
+ def testToDateTime2(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2, 30000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102.3Z').asDateTime
+
+ def testToDateTime3(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2, 30000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102,3Z').asDateTime
+
+ def testToDateTime4(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2, 30000, tzinfo=UTC) == useful.GeneralizedTime('20170711000102.3+0000').asDateTime
+
+ def testToDateTime5(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2, 30000, tzinfo=UTC2) == useful.GeneralizedTime('20170711000102.3+0200').asDateTime
+
+ def testToDateTime6(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2, 30000, tzinfo=UTC2) == useful.GeneralizedTime('20170711000102.3+02').asDateTime
+
+ def testToDateTime7(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1) == useful.GeneralizedTime('201707110001').asDateTime
+
+ def testToDateTime8(self):
+ assert datetime.datetime(2017, 7, 11, 0) == useful.GeneralizedTime('2017071100').asDateTime
+
+
+class UTCTimeTestCase(unittest.TestCase):
+
+ def testFromDateTime(self):
+ assert useful.UTCTime.fromDateTime(datetime.datetime(2017, 7, 11, 0, 1, 2, tzinfo=UTC)) == '170711000102Z'
+
+ def testToDateTime0(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2) == useful.UTCTime('170711000102').asDateTime
+
+ def testToDateTime1(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2, tzinfo=UTC) == useful.UTCTime('170711000102Z').asDateTime
+
+ def testToDateTime2(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2, tzinfo=UTC) == useful.UTCTime('170711000102+0000').asDateTime
+
+ def testToDateTime3(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1, 2, tzinfo=UTC2) == useful.UTCTime('170711000102+0200').asDateTime
+
+ def testToDateTime4(self):
+ assert datetime.datetime(2017, 7, 11, 0, 1) == useful.UTCTime('1707110001').asDateTime
+
+suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+
+if __name__ == '__main__':
+ unittest.TextTestRunner(verbosity=2).run(suite)