diff options
author | Ilya Etingof <etingof@gmail.com> | 2017-07-18 00:36:13 +0200 |
---|---|---|
committer | Ilya Etingof <etingof@gmail.com> | 2017-07-18 00:36:13 +0200 |
commit | 5a5f186eb9367e6fff602de5cc26300f071f8062 (patch) | |
tree | af76397a5cf2dcafb18f088d667750daa8f10998 | |
parent | d07ef6cb47c48eae585fc20ed17f804bd5dd9965 (diff) | |
parent | 3b177a33db98c19c6f2c6b8c90aaae12b6f2c6b0 (diff) | |
download | pyasn1-git-5a5f186eb9367e6fff602de5cc26300f071f8062.tar.gz |
Merge branch 'master' into open-types-support
-rw-r--r-- | CHANGES.rst | 12 | ||||
-rw-r--r-- | doc/source/docs/type/useful/generalizedtime.rst | 22 | ||||
-rw-r--r-- | doc/source/docs/type/useful/utctime.rst | 20 | ||||
-rw-r--r-- | pyasn1/__init__.py | 2 | ||||
-rw-r--r-- | pyasn1/type/useful.py | 144 | ||||
-rw-r--r-- | tests/type/__main__.py | 3 | ||||
-rw-r--r-- | tests/type/test_useful.py | 97 |
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) |