# coding: utf-8 """ Miscellaneous data helpers, including functions for converting integers to and from bytes and UTC timezone. Exports the following items: - OrderedDict() - int_from_bytes() - int_to_bytes() - timezone.utc - utc_with_dst - create_timezone() - inet_ntop() - inet_pton() - uri_to_iri() - iri_to_uri() """ from __future__ import unicode_literals, division, absolute_import, print_function import math import sys from datetime import datetime, date, timedelta, tzinfo from ._errors import unwrap from ._iri import iri_to_uri, uri_to_iri # noqa from ._ordereddict import OrderedDict # noqa from ._types import type_name if sys.platform == 'win32': from ._inet import inet_ntop, inet_pton else: from socket import inet_ntop, inet_pton # noqa # Python 2 if sys.version_info <= (3,): def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string :param value: The integer to convert :param signed: If the byte string should be encoded using two's complement :param width: If None, the minimal possible size (but at least 1), otherwise an integer of the byte width for the return value :return: A byte string """ if value == 0 and width == 0: return b'' # Handle negatives in two's complement is_neg = False if signed and value < 0: is_neg = True bits = int(math.ceil(len('%x' % abs(value)) / 2.0) * 8) value = (value + (1 << bits)) % (1 << bits) hex_str = '%x' % value if len(hex_str) & 1: hex_str = '0' + hex_str output = hex_str.decode('hex') if signed and not is_neg and ord(output[0:1]) & 0x80: output = b'\x00' + output if width is not None: if len(output) > width: raise OverflowError('int too big to convert') if is_neg: pad_char = b'\xFF' else: pad_char = b'\x00' output = (pad_char * (width - len(output))) + output elif is_neg and ord(output[0:1]) & 0x80 == 0: output = b'\xFF' + output return output def int_from_bytes(value, signed=False): """ Converts a byte string to an integer :param value: The byte string to convert :param signed: If the byte string should be interpreted using two's complement :return: An integer """ if value == b'': return 0 num = long(value.encode("hex"), 16) # noqa if not signed: return num # Check for sign bit and handle two's complement if ord(value[0:1]) & 0x80: bit_len = len(value) * 8 return num - (1 << bit_len) return num class timezone(tzinfo): # noqa """ Implements datetime.timezone for py2. Only full minute offsets are supported. DST is not supported. """ def __init__(self, offset, name=None): """ :param offset: A timedelta with this timezone's offset from UTC :param name: Name of the timezone; if None, generate one. """ if not timedelta(hours=-24) < offset < timedelta(hours=24): raise ValueError('Offset must be in [-23:59, 23:59]') if offset.seconds % 60 or offset.microseconds: raise ValueError('Offset must be full minutes') self._offset = offset if name is not None: self._name = name elif not offset: self._name = 'UTC' else: self._name = 'UTC' + _format_offset(offset) def __eq__(self, other): """ Compare two timezones :param other: The other timezone to compare to :return: A boolean """ if type(other) != timezone: return False return self._offset == other._offset def __getinitargs__(self): """ Called by tzinfo.__reduce__ to support pickle and copy. :return: offset and name, to be used for __init__ """ return self._offset, self._name def tzname(self, dt): """ :param dt: A datetime object; ignored. :return: Name of this timezone """ return self._name def utcoffset(self, dt): """ :param dt: A datetime object; ignored. :return: A timedelta object with the offset from UTC """ return self._offset def dst(self, dt): """ :param dt: A datetime object; ignored. :return: Zero timedelta """ return timedelta(0) timezone.utc = timezone(timedelta(0)) # Python 3 else: from datetime import timezone # noqa def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string :param value: The integer to convert :param signed: If the byte string should be encoded using two's complement :param width: If None, the minimal possible size (but at least 1), otherwise an integer of the byte width for the return value :return: A byte string """ if width is None: if signed: if value < 0: bits_required = abs(value + 1).bit_length() else: bits_required = value.bit_length() if bits_required % 8 == 0: bits_required += 1 else: bits_required = value.bit_length() width = math.ceil(bits_required / 8) or 1 return value.to_bytes(width, byteorder='big', signed=signed) def int_from_bytes(value, signed=False): """ Converts a byte string to an integer :param value: The byte string to convert :param signed: If the byte string should be interpreted using two's complement :return: An integer """ return int.from_bytes(value, 'big', signed=signed) def _format_offset(off): """ Format a timedelta into "[+-]HH:MM" format or "" for None """ if off is None: return '' mins = off.days * 24 * 60 + off.seconds // 60 sign = '-' if mins < 0 else '+' return sign + '%02d:%02d' % divmod(abs(mins), 60) class _UtcWithDst(tzinfo): """ Utc class where dst does not return None; required for astimezone """ def tzname(self, dt): return 'UTC' def utcoffset(self, dt): return timedelta(0) def dst(self, dt): return timedelta(0) utc_with_dst = _UtcWithDst() _timezone_cache = {} def create_timezone(offset): """ Returns a new datetime.timezone object with the given offset. Uses cached objects if possible. :param offset: A datetime.timedelta object; It needs to be in full minutes and between -23:59 and +23:59. :return: A datetime.timezone object """ try: tz = _timezone_cache[offset] except KeyError: tz = _timezone_cache[offset] = timezone(offset) return tz class extended_date(object): """ A datetime.datetime-like object that represents the year 0. This is just to handle 0000-01-01 found in some certificates. Python's datetime does not support year 0. The proleptic gregorian calendar repeats itself every 400 years. Therefore, the simplest way to format is to substitute year 2000. """ def __init__(self, year, month, day): """ :param year: The integer 0 :param month: An integer from 1 to 12 :param day: An integer from 1 to 31 """ if year != 0: raise ValueError('year must be 0') self._y2k = date(2000, month, day) @property def year(self): """ :return: The integer 0 """ return 0 @property def month(self): """ :return: An integer from 1 to 12 """ return self._y2k.month @property def day(self): """ :return: An integer from 1 to 31 """ return self._y2k.day def strftime(self, format): """ Formats the date using strftime() :param format: A strftime() format string :return: A str, the formatted date as a unicode string in Python 3 and a byte string in Python 2 """ # Format the date twice, once with year 2000, once with year 4000. # The only differences in the result will be in the millennium. Find them and replace by zeros. y2k = self._y2k.strftime(format) y4k = self._y2k.replace(year=4000).strftime(format) return ''.join('0' if (c2, c4) == ('2', '4') else c2 for c2, c4 in zip(y2k, y4k)) def isoformat(self): """ Formats the date as %Y-%m-%d :return: The date formatted to %Y-%m-%d as a unicode string in Python 3 and a byte string in Python 2 """ return self.strftime('0000-%m-%d') def replace(self, year=None, month=None, day=None): """ Returns a new datetime.date or asn1crypto.util.extended_date object with the specified components replaced :return: A datetime.date or asn1crypto.util.extended_date object """ if year is None: year = self.year if month is None: month = self.month if day is None: day = self.day if year > 0: cls = date else: cls = extended_date return cls( year, month, day ) def __str__(self): """ :return: A str representing this extended_date, e.g. "0000-01-01" """ return self.strftime('%Y-%m-%d') def __eq__(self, other): """ Compare two extended_date objects :param other: The other extended_date to compare to :return: A boolean """ # datetime.date object wouldn't compare equal because it can't be year 0 if not isinstance(other, self.__class__): return False return self.__cmp__(other) == 0 def __ne__(self, other): """ Compare two extended_date objects :param other: The other extended_date to compare to :return: A boolean """ return not self.__eq__(other) def _comparison_error(self, other): raise TypeError(unwrap( ''' An asn1crypto.util.extended_date object can only be compared to an asn1crypto.util.extended_date or datetime.date object, not %s ''', type_name(other) )) def __cmp__(self, other): """ Compare two extended_date or datetime.date objects :param other: The other extended_date object to compare to :return: An integer smaller than, equal to, or larger than 0 """ # self is year 0, other is >= year 1 if isinstance(other, date): return -1 if not isinstance(other, self.__class__): self._comparison_error(other) if self._y2k < other._y2k: return -1 if self._y2k > other._y2k: return 1 return 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __ge__(self, other): return self.__cmp__(other) >= 0 class extended_datetime(object): """ A datetime.datetime-like object that represents the year 0. This is just to handle 0000-01-01 found in some certificates. Python's datetime does not support year 0. The proleptic gregorian calendar repeats itself every 400 years. Therefore, the simplest way to format is to substitute year 2000. """ # There are 97 leap days during 400 years. DAYS_IN_400_YEARS = 400 * 365 + 97 DAYS_IN_2000_YEARS = 5 * DAYS_IN_400_YEARS def __init__(self, year, *args, **kwargs): """ :param year: The integer 0 :param args: Other positional arguments; see datetime.datetime. :param kwargs: Other keyword arguments; see datetime.datetime. """ if year != 0: raise ValueError('year must be 0') self._y2k = datetime(2000, *args, **kwargs) @property def year(self): """ :return: The integer 0 """ return 0 @property def month(self): """ :return: An integer from 1 to 12 """ return self._y2k.month @property def day(self): """ :return: An integer from 1 to 31 """ return self._y2k.day @property def hour(self): """ :return: An integer from 1 to 24 """ return self._y2k.hour @property def minute(self): """ :return: An integer from 1 to 60 """ return self._y2k.minute @property def second(self): """ :return: An integer from 1 to 60 """ return self._y2k.second @property def microsecond(self): """ :return: An integer from 0 to 999999 """ return self._y2k.microsecond @property def tzinfo(self): """ :return: If object is timezone aware, a datetime.tzinfo object, else None. """ return self._y2k.tzinfo def utcoffset(self): """ :return: If object is timezone aware, a datetime.timedelta object, else None. """ return self._y2k.utcoffset() def time(self): """ :return: A datetime.time object """ return self._y2k.time() def date(self): """ :return: An asn1crypto.util.extended_date of the date """ return extended_date(0, self.month, self.day) def strftime(self, format): """ Performs strftime(), always returning a str :param format: A strftime() format string :return: A str of the formatted datetime """ # Format the datetime twice, once with year 2000, once with year 4000. # The only differences in the result will be in the millennium. Find them and replace by zeros. y2k = self._y2k.strftime(format) y4k = self._y2k.replace(year=4000).strftime(format) return ''.join('0' if (c2, c4) == ('2', '4') else c2 for c2, c4 in zip(y2k, y4k)) def isoformat(self, sep='T'): """ Formats the date as "%Y-%m-%d %H:%M:%S" with the sep param between the date and time portions :param set: A single character of the separator to place between the date and time :return: The formatted datetime as a unicode string in Python 3 and a byte string in Python 2 """ s = '0000-%02d-%02d%c%02d:%02d:%02d' % (self.month, self.day, sep, self.hour, self.minute, self.second) if self.microsecond: s += '.%06d' % self.microsecond return s + _format_offset(self.utcoffset()) def replace(self, year=None, *args, **kwargs): """ Returns a new datetime.datetime or asn1crypto.util.extended_datetime object with the specified components replaced :param year: The new year to substitute. None to keep it. :param args: Other positional arguments; see datetime.datetime.replace. :param kwargs: Other keyword arguments; see datetime.datetime.replace. :return: A datetime.datetime or asn1crypto.util.extended_datetime object """ if year: return self._y2k.replace(year, *args, **kwargs) return extended_datetime.from_y2k(self._y2k.replace(2000, *args, **kwargs)) def astimezone(self, tz): """ Convert this extended_datetime to another timezone. :param tz: A datetime.tzinfo object. :return: A new extended_datetime or datetime.datetime object """ return extended_datetime.from_y2k(self._y2k.astimezone(tz)) def timestamp(self): """ Return POSIX timestamp. Only supported in python >= 3.3 :return: A float representing the seconds since 1970-01-01 UTC. This will be a negative value. """ return self._y2k.timestamp() - self.DAYS_IN_2000_YEARS * 86400 def __str__(self): """ :return: A str representing this extended_datetime, e.g. "0000-01-01 00:00:00.000001-10:00" """ return self.isoformat(sep=' ') def __eq__(self, other): """ Compare two extended_datetime objects :param other: The other extended_datetime to compare to :return: A boolean """ # Only compare against other datetime or extended_datetime objects if not isinstance(other, (self.__class__, datetime)): return False # Offset-naive and offset-aware datetimes are never the same if (self.tzinfo is None) != (other.tzinfo is None): return False return self.__cmp__(other) == 0 def __ne__(self, other): """ Compare two extended_datetime objects :param other: The other extended_datetime to compare to :return: A boolean """ return not self.__eq__(other) def _comparison_error(self, other): """ Raises a TypeError about the other object not being suitable for comparison :param other: The object being compared to """ raise TypeError(unwrap( ''' An asn1crypto.util.extended_datetime object can only be compared to an asn1crypto.util.extended_datetime or datetime.datetime object, not %s ''', type_name(other) )) def __cmp__(self, other): """ Compare two extended_datetime or datetime.datetime objects :param other: The other extended_datetime or datetime.datetime object to compare to :return: An integer smaller than, equal to, or larger than 0 """ if not isinstance(other, (self.__class__, datetime)): self._comparison_error(other) if (self.tzinfo is None) != (other.tzinfo is None): raise TypeError("can't compare offset-naive and offset-aware datetimes") diff = self - other zero = timedelta(0) if diff < zero: return -1 if diff > zero: return 1 return 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __ge__(self, other): return self.__cmp__(other) >= 0 def __add__(self, other): """ Adds a timedelta :param other: A datetime.timedelta object to add. :return: A new extended_datetime or datetime.datetime object. """ return extended_datetime.from_y2k(self._y2k + other) def __sub__(self, other): """ Subtracts a timedelta or another datetime. :param other: A datetime.timedelta or datetime.datetime or extended_datetime object to subtract. :return: If a timedelta is passed, a new extended_datetime or datetime.datetime object. Else a datetime.timedelta object. """ if isinstance(other, timedelta): return extended_datetime.from_y2k(self._y2k - other) if isinstance(other, extended_datetime): return self._y2k - other._y2k if isinstance(other, datetime): return self._y2k - other - timedelta(days=self.DAYS_IN_2000_YEARS) return NotImplemented def __rsub__(self, other): return -(self - other) @classmethod def from_y2k(cls, value): """ Revert substitution of year 2000. :param value: A datetime.datetime object which is 2000 years in the future. :return: A new extended_datetime or datetime.datetime object. """ year = value.year - 2000 if year > 0: new_cls = datetime else: new_cls = cls return new_cls( year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond, value.tzinfo )