├── .gitignore ├── LICENSE.rst ├── Makefile ├── README.rst ├── doc ├── conf.py └── index.rst ├── pytest.ini ├── requirements-dev.txt ├── requirements-test.txt ├── setup.cfg ├── setup.py ├── temporenc ├── __init__.py └── temporenc.py ├── tests └── test_temporenc.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Packaging cruft 2 | /*.egg-info/ 3 | 4 | # Byte code 5 | __pycache__/ 6 | *.py[co] 7 | 8 | # Testing cruft 9 | /.tox/ 10 | /.coverage 11 | htmlcov/ 12 | 13 | # Documentation 14 | /doc/build/ 15 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | Copyright © 2014–2017, Wouter Bolsterlee 5 | 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, this 15 | list of conditions and the following disclaimer in the documentation and/or 16 | other materials provided with the distribution. 17 | 18 | * Neither the name of the author nor the names of its contributors may be used 19 | to endorse or promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | *(This is the OSI approved 3-clause "New BSD License".)* 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: all doc test 3 | 4 | all: 5 | 6 | doc: 7 | @echo 8 | @echo "Building documentation" 9 | @echo "======================" 10 | @echo 11 | python setup.py build_sphinx 12 | @echo 13 | @echo Generated documentation: "file://"$$(readlink -f doc/build/html/index.html) 14 | @echo 15 | 16 | test: 17 | @echo 18 | @echo "Running tests" 19 | @echo "=============" 20 | @echo 21 | py.test tests/ 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Temporenc for Python 3 | ==================== 4 | 5 | This is a Python library implementing the `temporenc format 6 | `_. 7 | 8 | * `Online documentation `_ 9 | * `Project page (Github) `_ 10 | * `Temporenc website `_ 11 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | import temporenc 5 | 6 | 7 | # 8 | # Project settings 9 | # 10 | 11 | project = 'Temporenc' 12 | now = datetime.datetime.now() 13 | if now.year > 2014: 14 | copyright = '2014-{0}, Wouter Bolsterlee'.format(now.year) 15 | else: 16 | copyright = '2014, Wouter Bolsterlee' 17 | version = temporenc.__version__ 18 | release = temporenc.__version__ 19 | 20 | # 21 | # Extensions 22 | # 23 | 24 | extensions = [ 25 | 'sphinx.ext.autodoc', 26 | 'sphinx.ext.coverage', 27 | ] 28 | 29 | autodoc_member_order = 'bysource' 30 | 31 | # 32 | # Files and paths 33 | # 34 | 35 | master_doc = 'index' 36 | templates_path = ['_templates'] 37 | source_suffix = '.rst' 38 | exclude_patterns = ['build'] 39 | 40 | 41 | # 42 | # Output 43 | # 44 | 45 | pygments_style = 'sphinx' 46 | html_theme = 'default' 47 | html_static_path = ['_static'] 48 | html_domain_indices = False 49 | html_use_index = False 50 | html_show_sphinx = False 51 | html_show_copyright = True 52 | 53 | 54 | # 55 | # These docs are intended for hosting by readthedocs.org. Override some 56 | # settings for local use. 57 | # 58 | 59 | if not 'READTHEDOCS' in os.environ: 60 | import sphinx_rtd_theme 61 | html_theme = 'sphinx_rtd_theme' 62 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 63 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | ****************************** 2 | Python library for *temporenc* 3 | ****************************** 4 | 5 | This is a Python library implementing the `temporenc format 6 | `_ for dates and times. 7 | 8 | Features: 9 | 10 | * Support for all *temporenc* types 11 | 12 | * Interoperability with the ``datetime`` module 13 | 14 | * Time zone support, including conversion to local time 15 | 16 | * Compatibility with both Python 2 (2.6+) and Python 3 (3.2+) 17 | 18 | * Decent performance 19 | 20 | * Permissive BSD license 21 | 22 | ____ 23 | 24 | 25 | .. rubric:: Contents 26 | 27 | .. contents:: 28 | :local: 29 | 30 | ____ 31 | 32 | 33 | Installation 34 | ============ 35 | 36 | 37 | Use ``pip`` to install the library (e.g. into a ``virtualenv``): 38 | 39 | .. code-block:: shell-session 40 | 41 | $ pip install temporenc 42 | 43 | ____ 44 | 45 | 46 | Usage 47 | ===== 48 | 49 | .. py:currentmodule:: temporenc 50 | 51 | Basic usage 52 | ----------- 53 | 54 | All functionality is provided by a single module with the name ``temporenc``:: 55 | 56 | >>> import temporenc 57 | 58 | To encode date and time information into a byte string, use the :py:func:`packb` 59 | function:: 60 | 61 | >>> temporenc.packb(year=2014, month=10, day=23) 62 | b'\x8f\xbd6' 63 | 64 | This function automatically determines the most compact representation for the 65 | provided information. In this case, the result uses *temporenc* type ``D``, but 66 | if you want to use a different type, you can provide it explicitly:: 67 | 68 | >>> temporenc.packb(type='DT', year=2014, month=10, day=23) 69 | b'\x1fzm\xff\xff' 70 | 71 | To unpack a byte string, use :py:func:`unpackb`:: 72 | 73 | >>> moment = temporenc.unpackb(b'\x1fzm\xff\xff') 74 | >>> moment 75 | 76 | >>> print(moment) 77 | 2014-10-23 78 | 79 | As you can see, unpacking returns a :py:class:`Moment` instance. This class has 80 | a reasonable string representation, but it is generally more useful to access 81 | the individual components using one of its many attributes:: 82 | 83 | >>> print(moment.year) 84 | 2014 85 | >>> print(moment.month) 86 | 10 87 | >>> print(moment.day) 88 | 13 89 | >>> print(moment.second) 90 | None 91 | 92 | Since all fields are optional in *temporenc* values, and since no time 93 | information was set in this example, some of the attributes (e.g. `second`) are 94 | `None`. 95 | 96 | Integration with the ``datetime`` module 97 | ---------------------------------------- 98 | 99 | Python has built-in support for date and time handling, provided by the 100 | ``datetime`` module in the standard library, which is how applications usually 101 | work with date and time information. Instead of specifying all the fields 102 | manually when packing data, which is cumbersome and error-prone, the 103 | ``temporenc`` module integrates with the built-in ``datetime`` module:: 104 | 105 | >>> import datetime 106 | >>> now = datetime.datetime.now() 107 | >>> now 108 | datetime.datetime(2014, 10, 23, 18, 45, 23, 612883) 109 | >>> temporenc.packb(now) 110 | b'W\xde\x9bJ\xd5\xe5hL' 111 | 112 | As you can see, instead of specifying all the components manually, instances of 113 | the built-in ``datetime.datetime`` class can be passed directly as the first 114 | argument to :py:func:`packb`. This also works for ``datetime.date`` and 115 | ``datetime.time`` instances. 116 | 117 | Since the Python ``datetime`` module *always* uses microsecond precision, this 118 | library defaults to *temporenc* types with sub-second precision (e.g. ``DTS``) 119 | when an instance of one of the ``datetime`` classes is passed. If no subsecond 120 | precision is required, you can specify a different type to save space:: 121 | 122 | >>> temporenc.packb(now, type='DT') 123 | b'\x1fzm+W' 124 | 125 | The integration with the ``datetime`` module works both ways. Instances of the 126 | :py:class:`Moment` class (as returned by the unpacking functions) can be 127 | converted to the standard date and time classes using the 128 | :py:meth:`~Moment.datetime`, :py:meth:`~Moment.date`, and 129 | :py:meth:`~Moment.time` methods:: 130 | 131 | >>> moment = temporenc.unpackb(b'W\xde\x9bJ\xd5\xe5hL') 132 | >>> moment 133 | 134 | >>> moment.datetime() 135 | datetime.datetime(2014, 10, 23, 18, 45, 23, 612883) 136 | >>> moment.date() 137 | datetime.date(2014, 10, 23) 138 | >>> moment.time() 139 | datetime.time(18, 45, 23, 612883) 140 | 141 | Conversion to and from classes from the ``datetime`` module have full time zone 142 | support. See the API docs for :py:meth:`Moment.datetime` for more details about 143 | time zone handling. 144 | 145 | .. warning:: 146 | 147 | The Python ``temporenc`` module only concerns itself with encoding and 148 | decoding. It does *not* do any date and time calculations, and hence does not 149 | validate that dates are correct. For example, it handles the non-existent 150 | date `February 30` just fine. Always convert to native classes from the 151 | ``datetime`` module if you need to work with date and time information in 152 | your application. 153 | 154 | 155 | Working with file-like objects 156 | ------------------------------ 157 | 158 | The *temporenc* encoding format allows for reading data from a stream without 159 | knowing in advance how big the encoded byte string is. This library supports 160 | this through the :py:func:`unpack` function, which consumes exactly the required 161 | number of bytes from the stream:: 162 | 163 | >>> import io 164 | >>> fp = io.BytesIO() # this could be a real file 165 | >>> fp.write(b'W\xde\x9bJ\xd5\xe5hL') 166 | >>> fp.write(b'foo') 167 | >>> fp.seek(0) 168 | >>> temporenc.unpack(fp) 169 | 170 | >>> fp.tell() 171 | 8 172 | >>> fp.read() 173 | b'foo' 174 | 175 | For writing directly to a file-like object, the :py:func:`pack` function can be 176 | used, though this is just a shortcut. 177 | 178 | ____ 179 | 180 | 181 | API 182 | === 183 | 184 | The :py:func:`packb` and :py:func:`unpackb` functions operate on byte strings. 185 | 186 | .. autofunction:: packb 187 | .. autofunction:: unpackb 188 | 189 | The :py:func:`pack` and :py:func:`unpack` functions operate on file-like 190 | objects. 191 | 192 | .. autofunction:: pack 193 | .. autofunction:: unpack 194 | 195 | Both :py:func:`unpackb` and :py:func:`unpack` return an instance of the 196 | :py:class:`Moment` class. 197 | 198 | .. autoclass:: Moment 199 | :members: 200 | 201 | ____ 202 | 203 | 204 | Contributing 205 | ============ 206 | 207 | Source code, including the test suite, is maintained at Github: 208 | 209 | `temporenc-python on github `_ 210 | 211 | Feel free to submit feedback, report issues, bring up improvement ideas, and 212 | contribute fixes! 213 | 214 | ____ 215 | 216 | 217 | Version history 218 | =============== 219 | 220 | * x.y (not yet released) 221 | 222 | * no longer perform utc conversion, see 223 | `temporenc#8 `_ 224 | 225 | * 0.1 226 | 227 | Release date: 2014-10-30 228 | 229 | Initial public release. 230 | 231 | ____ 232 | 233 | .. license is in a separate file 234 | 235 | .. include:: ../LICENSE.rst 236 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --verbose --showlocals --cov temporenc --cov-report html --cov-report term-missing 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-rtd-theme 3 | tox 4 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = doc/ 3 | build-dir = doc/build/ 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | # No third party dependencies, so importing the package should be safe. 5 | import temporenc 6 | 7 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as fp: 8 | long_description = fp.read() 9 | 10 | setup( 11 | name='temporenc', 12 | description="Python library for the temporenc format", 13 | long_description=long_description, 14 | version=temporenc.__version__, 15 | author="Wouter Bolsterlee", 16 | author_email="uws@xs4all.nl", 17 | url='https://github.com/wbolster/temporenc-python', 18 | packages=['temporenc'], 19 | license='BSD', 20 | classifiers=[ 21 | 'Development Status :: 4 - Beta', 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: BSD License', 24 | 'Programming Language :: Python', 25 | 'Programming Language :: Python :: 2', 26 | 'Programming Language :: Python :: 3', 27 | 'Topic :: Database', 28 | 'Topic :: Database :: Database Engines/Servers', 29 | 'Topic :: Software Development :: Libraries', 30 | 'Topic :: Software Development :: Libraries :: Python Modules', 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /temporenc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Temporenc, a comprehensive binary encoding format for dates and times 3 | """ 4 | 5 | __version__ = '0.1.0' 6 | __version_info__ = tuple(map(int, __version__.split('.'))) 7 | 8 | 9 | # Export public API 10 | from .temporenc import ( # noqa 11 | pack, 12 | packb, 13 | unpack, 14 | unpackb, 15 | Moment, 16 | ) 17 | -------------------------------------------------------------------------------- /temporenc/temporenc.py: -------------------------------------------------------------------------------- 1 | 2 | import datetime 3 | import struct 4 | import sys 5 | 6 | 7 | # 8 | # Compatibility 9 | # 10 | 11 | PY2 = sys.version_info[0] == 2 12 | PY26 = sys.version_info[0:2] == (2, 6) 13 | 14 | 15 | # 16 | # Components and types 17 | # 18 | 19 | SUPPORTED_TYPES = set(['D', 'T', 'DT', 'DTZ', 'DTS', 'DTSZ']) 20 | 21 | D_MASK = 0x1fffff 22 | T_MASK = 0x1ffff 23 | Z_MASK = 0x7f 24 | 25 | YEAR_MAX, YEAR_EMPTY, YEAR_MASK = 4094, 4095, 0xfff 26 | MONTH_MAX, MONTH_EMPTY, MONTH_MASK = 11, 15, 0xf 27 | DAY_MAX, DAY_EMPTY, DAY_MASK = 30, 31, 0x1f 28 | HOUR_MAX, HOUR_EMPTY, HOUR_MASK = 23, 31, 0x1f 29 | MINUTE_MAX, MINUTE_EMPTY, MINUTE_MASK = 59, 63, 0x3f 30 | SECOND_MAX, SECOND_EMPTY, SECOND_MASK = 60, 63, 0x3f 31 | MILLISECOND_MAX, MILLISECOND_MASK = 999, 0x3ff 32 | MICROSECOND_MAX, MICROSECOND_MASK = 999999, 0xfffff 33 | NANOSECOND_MAX, NANOSECOND_MASK = 999999999, 0x3fffffff 34 | TIMEZONE_MAX, TIMEZONE_EMPTY = 125, 127 35 | 36 | D_LENGTH = 3 37 | T_LENGTH = 3 38 | DT_LENGTH = 5 39 | DTZ_LENGTH = 6 40 | DTS_LENGTHS = [7, 8, 9, 6] # indexed by precision bits 41 | DTSZ_LENGTHS = [8, 9, 10, 7] # idem 42 | 43 | 44 | # 45 | # Helpers 46 | # 47 | 48 | pack_4 = struct.Struct('>L').pack 49 | pack_8 = struct.Struct('>Q').pack 50 | pack_2_8 = struct.Struct('>HQ').pack 51 | 52 | 53 | def unpack_4(value, _unpack=struct.Struct('>L').unpack): 54 | return _unpack(value)[0] 55 | 56 | 57 | def unpack_8(value, _unpack=struct.Struct('>Q').unpack): 58 | return _unpack(value)[0] 59 | 60 | 61 | def _detect_type(first): 62 | """ 63 | Detect type information from the numerical value of the first byte. 64 | """ 65 | if first <= 0b00111111: 66 | return 'DT', None, DT_LENGTH 67 | elif first <= 0b01111111: 68 | precision = first >> 4 & 0b11 69 | return 'DTS', precision, DTS_LENGTHS[precision] 70 | elif first <= 0b10011111: 71 | return 'D', None, D_LENGTH 72 | elif first <= 0b10100001: 73 | return 'T', None, T_LENGTH 74 | elif first <= 0b10111111: 75 | return None, None, None 76 | elif first <= 0b11011111: 77 | return 'DTZ', None, DTZ_LENGTH 78 | elif first <= 0b11111111: 79 | precision = first >> 3 & 0b11 80 | return 'DTSZ', precision, DTSZ_LENGTHS[precision] 81 | 82 | 83 | class FixedOffset(datetime.tzinfo): 84 | """Time zone information for a fixed offset from UTC.""" 85 | 86 | # Python 2 does not have any concrete tzinfo implementations in its 87 | # standard library, hence this implementation. This implementation 88 | # is based on the examples in the Python docs, in particular: 89 | # https://docs.python.org/3.4/library/datetime.html#tzinfo-objects 90 | 91 | ZERO = datetime.timedelta(0) 92 | 93 | def __init__(self, minutes): 94 | self._offset = datetime.timedelta(minutes=minutes) 95 | sign = '+' if minutes >= 0 else '-' 96 | hours, minutes = divmod(minutes, 60) 97 | self._name = 'UTC{0}{1:02d}:{2:02d}'.format(sign, hours, minutes) 98 | 99 | def utcoffset(self, dt): 100 | return self._offset 101 | 102 | def tzname(self, dt): 103 | return self._name 104 | 105 | def dst(self, dt): 106 | return self.ZERO 107 | 108 | def __repr__(self): 109 | return '<{0}>'.format(self._name) 110 | 111 | 112 | # This cache maps offsets in minutes to FixedOffset instances. 113 | tzinfo_cache = { 114 | None: None, # hack to simpify cached_tzinfo() callers 115 | } 116 | 117 | 118 | def cached_tzinfo(minutes): 119 | """ 120 | Get a (cached) tzinfo instance for the specified offset in minutes. 121 | """ 122 | try: 123 | tzinfo = tzinfo_cache[minutes] 124 | except KeyError: 125 | tzinfo_cache[minutes] = tzinfo = FixedOffset(minutes) 126 | return tzinfo 127 | 128 | 129 | # 130 | # Public API 131 | # 132 | 133 | class Moment(object): 134 | """ 135 | Container to represent a parsed *temporenc* value. 136 | 137 | Each constituent part is accessible as an instance attribute. These 138 | are: ``year``, ``month``, ``day``, ``hour``, ``minute``, ``second``, 139 | ``millisecond``, ``microsecond``, ``nanosecond``, and ``tz_offset``. 140 | Since *temporenc* allows partial date and time information, any 141 | attribute can be ``None``. 142 | 143 | The attributes for sub-second precision form a group that is either 144 | completely empty (all attributes are ``None``) or completely filled 145 | (no attribute is ``None``). 146 | 147 | This class is intended to be a read-only immutable data structure; 148 | assigning new values to attributes is not supported. 149 | 150 | Instances are hashable and can be used as dictionary keys or as 151 | members of a set. Instances representing the same moment in time 152 | have the same hash value. Time zone information is not taken into 153 | account for hashing purposes, since time zone aware values must have 154 | their constituent parts in UTC. 155 | 156 | Instances of this class can be compared to each other, with earlier 157 | dates sorting first. As with hashing, time zone information is not 158 | taken into account, since the actual data must be in UTC in those 159 | cases. 160 | 161 | .. note:: 162 | 163 | This class must not be instantiated directly; use one of the 164 | unpacking functions like :py:func:`unpackb()` instead. 165 | """ 166 | __slots__ = [ 167 | 'year', 'month', 'day', 168 | 'hour', 'minute', 'second', 169 | 'millisecond', 'microsecond', 'nanosecond', 170 | 'tz_offset', 171 | '_has_date', '_has_time', '_struct'] 172 | 173 | def __init__( 174 | self, 175 | year, month, day, 176 | hour, minute, second, nanosecond, 177 | tz_offset): 178 | 179 | #: Year component. 180 | self.year = year 181 | 182 | #: Month component. 183 | self.month = month 184 | 185 | #: Day component. 186 | self.day = day 187 | 188 | #: Hour component. 189 | self.hour = hour 190 | 191 | #: Minute component. 192 | self.minute = minute 193 | 194 | #: Second component. 195 | self.second = second 196 | 197 | if nanosecond is None: 198 | self.millisecond = self.microsecond = self.nanosecond = None 199 | else: 200 | #: Millisecond component. If set, :py:attr:`microsecond` and 201 | #: :py:attr:`nanosecond` are also set. 202 | self.millisecond = nanosecond // 1000000 203 | 204 | #: Microsecond component. If set, :py:attr:`millisecond` and 205 | #: :py:attr:`nanosecond` are also set. 206 | self.microsecond = nanosecond // 1000 207 | 208 | #: Nanosecond component. If set, :py:attr:`millisecond` and 209 | #: :py:attr:`microsecond` are also set. 210 | self.nanosecond = nanosecond 211 | 212 | #: Time zone offset (total minutes). To calculate the hours and 213 | #: minutes, use ``h, m = divmod(offset, 60)``. 214 | self.tz_offset = tz_offset 215 | 216 | self._has_date = not (year is None and month is None and day is None) 217 | self._has_time = not (hour is None and minute is None 218 | and second is None) 219 | 220 | # This 'struct' contains the values that are relevant for 221 | # comparison, hashing, and so on. 222 | self._struct = ( 223 | self.year, self.month, self.day, 224 | self.hour, self.minute, self.second, self.nanosecond, 225 | self.tz_offset) 226 | 227 | def __str__(self): 228 | buf = [] 229 | 230 | if self._has_date: 231 | buf.append("{0:04d}-".format(self.year) 232 | if self.year is not None else "????-") 233 | buf.append("{0:02d}-".format(self.month) 234 | if self.month is not None else "??-") 235 | buf.append("{0:02d}".format(self.day) 236 | if self.day is not None else "??") 237 | 238 | if self._has_time: 239 | 240 | if self._has_date: 241 | buf.append(" ") # separator 242 | 243 | buf.append("{0:02d}:".format(self.hour) 244 | if self.hour is not None else "??:") 245 | buf.append("{0:02d}:".format(self.minute) 246 | if self.minute is not None else "??:") 247 | buf.append("{0:02d}".format(self.second) 248 | if self.second is not None else "??") 249 | 250 | if self.nanosecond is not None: 251 | if not self._has_time: 252 | # Weird edge case: empty hour/minute/second, but 253 | # sub-second precision is set. 254 | buf.append("??:??:??") 255 | 256 | if self.nanosecond == 0: 257 | buf.append('.0') 258 | else: 259 | buf.append(".{0:09d}".format(self.nanosecond).rstrip("0")) 260 | 261 | if self.tz_offset is not None: 262 | if self.tz_offset == 0: 263 | buf.append('Z') 264 | else: 265 | h, m = divmod(self.tz_offset, 60) 266 | sign = '+' if h >= 0 else '-' 267 | buf.append('{0}{1:02d}:{2:02d}'.format(sign, h, m)) 268 | 269 | return ''.join(buf) 270 | 271 | def __repr__(self): 272 | return "".format(self) 273 | 274 | def __eq__(self, other): 275 | if not isinstance(other, type(self)): 276 | return NotImplemented 277 | return self._struct == other._struct 278 | 279 | def __ne__(self, other): 280 | if not isinstance(other, type(self)): 281 | return NotImplemented 282 | return self._struct != other._struct 283 | 284 | def __gt__(self, other): 285 | if not isinstance(other, type(self)): 286 | return NotImplemented 287 | return self._struct > other._struct 288 | 289 | def __ge__(self, other): 290 | if not isinstance(other, type(self)): 291 | return NotImplemented 292 | return self._struct >= other._struct 293 | 294 | def __lt__(self, other): 295 | if not isinstance(other, type(self)): 296 | return NotImplemented 297 | return self._struct < other._struct 298 | 299 | def __le__(self, other): 300 | if not isinstance(other, type(self)): 301 | return NotImplemented 302 | return self._struct <= other._struct 303 | 304 | def __hash__(self): 305 | return hash(self._struct) 306 | 307 | def datetime(self, strict=True): 308 | """ 309 | Convert this value to a ``datetime.datetime`` instance. 310 | 311 | Since the classes in the ``datetime`` module do not support 312 | missing values, this will fail when one of the required 313 | components is not set, which is indicated by raising 314 | a :py:exc:`ValueError`. 315 | 316 | The default is to perform a strict conversion. To ease working 317 | with partial dates and times, the `strict` argument can be set 318 | to `False`. In that case this method will try to convert the 319 | value anyway, by substituting a default value for any missing 320 | component, e.g. a missing time is set to `00:00:00`. Note that 321 | these substituted values are bogus and should not be used for 322 | any application logic, but at least this allows applications to 323 | use things like ``.strftime()`` on partial dates and times. 324 | 325 | The *temporenc* format allows inclusion of a time zone offset. 326 | When converting to a ``datetime`` instance, time zone 327 | information is handled as follows: 328 | 329 | * When no time zone information was present in the original data 330 | (e.g. when unpacking *temporenc* type ``DT``), the return 331 | value will be a naive `datetime` instance, i.e. its ``tzinfo`` 332 | attribute is `None`. 333 | 334 | * If the original data did include time zone information, the 335 | return value will be a time zone aware instance, which means the 336 | instance will have a ``tzinfo`` attribute corresponding to 337 | the offset included in the value. 338 | 339 | :param bool strict: whether to use strict conversion rules 340 | :return: converted value 341 | :rtype: `datetime.datetime` 342 | """ 343 | 344 | if strict: 345 | if None in (self.year, self.month, self.day): 346 | raise ValueError("incomplete date information") 347 | if None in (self.hour, self.minute, self.second): 348 | raise ValueError("incomplete time information") 349 | 350 | hour, minute, second = self.hour, self.minute, self.second 351 | year, month, day = self.year, self.month, self.day 352 | 353 | # The stdlib's datetime classes always specify microseconds. 354 | us = self.microsecond if self.microsecond is not None else 0 355 | 356 | if not strict: 357 | # Substitute defaults for missing values. 358 | if year is None: 359 | year = 1 360 | 361 | if month is None: 362 | month = 1 363 | 364 | if day is None: 365 | day = 1 366 | 367 | if hour is None: 368 | hour = 0 369 | 370 | if minute is None: 371 | minute = 0 372 | 373 | if second is None: 374 | second = 0 375 | elif second == 60: # assume that this is a leap second 376 | second = 59 377 | 378 | dt = datetime.datetime( 379 | year, month, day, 380 | hour, minute, second, us, 381 | tzinfo=cached_tzinfo(self.tz_offset)) 382 | 383 | return dt 384 | 385 | def date(self, strict=True): 386 | """ 387 | Convert this value to a ``datetime.date`` instance. 388 | 389 | See the documentation for the :py:meth:`datetime()` method for 390 | more information. 391 | 392 | :param bool strict: whether to use strict conversion rules 393 | :param bool local: whether to convert to local time 394 | :return: converted value 395 | :rtype: `datetime.date` 396 | """ 397 | if strict: 398 | if None in (self.year, self.month, self.day): 399 | raise ValueError("incomplete date information") 400 | 401 | # Shortcut for performance reasons 402 | return datetime.date(self.year, self.month, self.day) 403 | 404 | return self.datetime(strict=False).date() 405 | 406 | def time(self, strict=True): 407 | """ 408 | Convert this value to a ``datetime.time`` instance. 409 | 410 | See the documentation for the :py:meth:`datetime()` method for 411 | more information. 412 | 413 | :param bool strict: whether to use strict conversion rules 414 | :param bool local: whether to convert to local time 415 | :return: converted value 416 | :rtype: `datetime.time` 417 | """ 418 | if strict: 419 | if None in (self.hour, self.minute, self.second): 420 | raise ValueError("incomplete time information") 421 | 422 | # Shortcut for performance reasons 423 | return datetime.time( 424 | self.hour, self.minute, self.second, 425 | self.microsecond if self.microsecond is not None else 0, 426 | tzinfo=cached_tzinfo(self.tz_offset)) 427 | 428 | return self.datetime(strict=False).timetz() 429 | 430 | 431 | def packb( 432 | value=None, type=None, 433 | year=None, month=None, day=None, 434 | hour=None, minute=None, second=None, 435 | millisecond=None, microsecond=None, nanosecond=None, 436 | tz_offset=None): 437 | """ 438 | Pack date and time information into a byte string. 439 | 440 | If specified, `value` must be a ``datetime.datetime``, 441 | ``datetime.date``, or ``datetime.time`` instance. 442 | 443 | The `type` specifies the *temporenc* type to use. Valid types are 444 | ``D``, ``T``, ``DT``, ``DTZ``, ``DTS``, or ``DTSZ``. If not 445 | specified, the most compact encoding that can represent the provided 446 | information will be determined automatically. Note that instances of 447 | the classes in the ``datetime`` module always use microsecond 448 | precision, so make sure to specify a more compact type if no 449 | sub-second precision is required. 450 | 451 | Most applications would only use the `value` and `type` arguments; 452 | the other arguments allow for encoding data that does not fit the 453 | conceptual date and time model used by the standard library's 454 | ``datetime`` module. 455 | 456 | .. note:: 457 | 458 | Applications that require lexicographical ordering of encoded 459 | values should always explicitly specify a type to use. 460 | 461 | All other arguments can be used to specify individual pieces of 462 | information that make up a date or time. If both `value` and more 463 | specific fields are provided, the individual fields override the 464 | values extracted from `value`, e.g. ``packb(datetime.datetime.now(), 465 | minute=0, second=0)`` encodes the start of the current hour. 466 | 467 | The sub-second precision arguments (`millisecond`, `microsecond`, 468 | and `nanosecond`) must not be used together, since those are 469 | conceptually mutually exclusive. 470 | 471 | .. note:: 472 | 473 | The `value` argument is the only positional argument. All other 474 | arguments *must* be specified as keyword arguments (even though 475 | this is not enforced because of Python 2 compatibility). 476 | 477 | :param value: instance of one of the ``datetime`` classes (optional) 478 | :param str type: *temporenc* type (optional) 479 | :param int year: year (optional) 480 | :param int month: month (optional) 481 | :param int day: day (optional) 482 | :param int hour: hour (optional) 483 | :param int minute: minute (optional) 484 | :param int second: second (optional) 485 | :param int millisecond: millisecond (optional) 486 | :param int microsecond: microsecond (optional) 487 | :param int nanosecond: nanosecond (optional) 488 | :param int tz_offset: time zone offset in minutes from UTC (optional) 489 | :return: encoded *temporenc* value 490 | :rtype: bytes 491 | """ 492 | 493 | # 494 | # Native 'datetime' module handling 495 | # 496 | 497 | if value is not None: 498 | handled = False 499 | 500 | if isinstance(value, (datetime.datetime, datetime.time)): 501 | 502 | # Handle time zone information. Instances of the 503 | # datetime.datetime and datetime.time classes may have an 504 | # associated time zone. If an explicit tz_offset was 505 | # specified, that takes precedence. 506 | if tz_offset is None: 507 | delta = value.utcoffset() 508 | if delta is not None: 509 | tz_offset = delta.days * 1440 + delta.seconds // 60 510 | 511 | # Extract time fields 512 | handled = True 513 | if hour is None: 514 | hour = value.hour 515 | if minute is None: 516 | minute = value.minute 517 | if second is None: 518 | second = value.second 519 | if (millisecond is None and microsecond is None 520 | and nanosecond is None): 521 | microsecond = value.microsecond 522 | 523 | if isinstance(value, (datetime.datetime, datetime.date)): 524 | # Extract date fields 525 | handled = True 526 | if year is None: 527 | year = value.year 528 | if month is None: 529 | month = value.month 530 | if day is None: 531 | day = value.day 532 | 533 | if not handled: 534 | raise ValueError("Cannot encode {0!r}".format(value)) 535 | 536 | # 537 | # Type detection 538 | # 539 | 540 | if type is None: 541 | has_d = not (year is None and month is None and day is None) 542 | has_t = not (hour is None and minute is None and second is None) 543 | has_s = not (millisecond is None and microsecond is None 544 | and nanosecond is None) 545 | has_z = tz_offset is not None 546 | 547 | if has_z and has_s: 548 | type = 'DTSZ' 549 | elif has_z: 550 | type = 'DTZ' 551 | elif has_s: 552 | type = 'DTS' 553 | elif has_d and has_t: 554 | type = 'DT' 555 | elif has_d: 556 | type = 'D' 557 | elif has_t: 558 | type = 'T' 559 | else: 560 | # No information at all, just use the smallest type 561 | type = 'D' 562 | 563 | elif type not in SUPPORTED_TYPES: 564 | raise ValueError("invalid temporenc type: {0!r}".format(type)) 565 | 566 | # 567 | # Value checking 568 | # 569 | 570 | if year is None: 571 | year = YEAR_EMPTY 572 | elif not 0 <= year <= YEAR_MAX: 573 | raise ValueError("year not within supported range") 574 | 575 | if month is None: 576 | month = MONTH_EMPTY 577 | else: 578 | month -= 1 579 | if not 0 <= month <= MONTH_MAX: 580 | raise ValueError("month not within supported range") 581 | 582 | if day is None: 583 | day = DAY_EMPTY 584 | else: 585 | day -= 1 586 | if not 0 <= day <= DAY_MAX: 587 | raise ValueError("day not within supported range") 588 | 589 | if hour is None: 590 | hour = HOUR_EMPTY 591 | elif not 0 <= hour <= HOUR_MAX: 592 | raise ValueError("hour not within supported range") 593 | 594 | if minute is None: 595 | minute = MINUTE_EMPTY 596 | elif not 0 <= minute <= MINUTE_MAX: 597 | raise ValueError("minute not within supported range") 598 | 599 | if second is None: 600 | second = SECOND_EMPTY 601 | elif not 0 <= second <= SECOND_MAX: 602 | raise ValueError("second not within supported range") 603 | 604 | if (millisecond is not None 605 | and not 0 <= millisecond <= MILLISECOND_MAX): 606 | raise ValueError("millisecond not within supported range") 607 | 608 | if (microsecond is not None 609 | and not 0 <= microsecond <= MICROSECOND_MAX): 610 | raise ValueError("microsecond not within supported range") 611 | 612 | if (nanosecond is not None 613 | and not 0 <= nanosecond <= NANOSECOND_MAX): 614 | raise ValueError("nanosecond not within supported range") 615 | 616 | if tz_offset is None: 617 | tz_offset = TIMEZONE_EMPTY 618 | else: 619 | z, remainder = divmod(tz_offset, 15) 620 | if remainder: 621 | raise ValueError("tz_offset must be a multiple of 15") 622 | z += 64 623 | if not 0 <= z <= TIMEZONE_MAX: 624 | raise ValueError("tz_offset not within supported range") 625 | 626 | # 627 | # Byte packing 628 | # 629 | 630 | d = year << 9 | month << 5 | day 631 | t = hour << 12 | minute << 6 | second 632 | 633 | if type == 'D': 634 | # 100DDDDD DDDDDDDD DDDDDDDD 635 | return pack_4(0b100 << 21 | d)[-3:] 636 | 637 | elif type == 'T': 638 | # 1010000T TTTTTTTT TTTTTTTT 639 | return pack_4(0b1010000 << 17 | t)[-3:] 640 | 641 | elif type == 'DT': 642 | # 00DDDDDD DDDDDDDD DDDDDDDT TTTTTTTT 643 | # TTTTTTTT 644 | return pack_8(d << 17 | t)[-5:] 645 | 646 | elif type == 'DTZ': 647 | # 110DDDDD DDDDDDDD DDDDDDDD TTTTTTTT 648 | # TTTTTTTT TZZZZZZZ 649 | return pack_8(0b110 << 45 | d << 24 | t << 7 | z)[-6:] 650 | 651 | elif type == 'DTS': 652 | if nanosecond is not None: 653 | # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT 654 | # TTTTTTTT TTSSSSSS SSSSSSSS SSSSSSSS 655 | # SSSSSSSS 656 | return pack_2_8( 657 | 0b0110 << 4 | d >> 17, 658 | (d & 0x1ffff) << 47 | t << 30 | nanosecond)[-9:] 659 | elif microsecond is not None: 660 | # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT 661 | # TTTTTTTT TTSSSSSS SSSSSSSS SSSSSS00 662 | return pack_8( 663 | 0b0101 << 60 | d << 39 | t << 22 | microsecond << 2) 664 | elif millisecond is not None: 665 | # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT 666 | # TTTTTTTT TTSSSSSS SSSS0000 667 | return pack_8( 668 | 0b0100 << 52 | d << 31 | t << 14 | millisecond << 4)[-7:] 669 | else: 670 | # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT 671 | # TTTTTTTT TT000000 672 | return pack_8(0b0111 << 44 | d << 23 | t << 6)[-6:] 673 | 674 | elif type == 'DTSZ': 675 | if nanosecond is not None: 676 | # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT 677 | # TTTTTTTT TTTSSSSS SSSSSSSS SSSSSSSS 678 | # SSSSSSSS SZZZZZZZ 679 | return pack_2_8( 680 | 0b11110 << 11 | d >> 10, 681 | (d & 0x3ff) << 54 | t << 37 | nanosecond << 7 | z) 682 | elif microsecond is not None: 683 | # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT 684 | # TTTTTTTT TTTSSSSS SSSSSSSS SSSSSSSZ 685 | # ZZZZZZ00 686 | return pack_2_8( 687 | 0b11101 << 3 | d >> 18, 688 | (d & 0x3ffff) << 46 | t << 29 | microsecond << 9 | z << 2)[-9:] 689 | elif millisecond is not None: 690 | # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT 691 | # TTTTTTTT TTTSSSSS SSSSSZZZ ZZZZ0000 692 | return pack_8( 693 | 0b11100 << 59 | d << 38 | t << 21 | millisecond << 11 | z << 4) 694 | else: 695 | # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT 696 | # TTTTTTTT TTTZZZZZ ZZ000000 697 | return pack_8(0b11111 << 51 | d << 30 | t << 13 | z << 6)[-7:] 698 | 699 | 700 | def pack(fp, *args, **kwargs): 701 | """ 702 | Pack date and time information and write it to a file-like object. 703 | 704 | This is a short-hand for writing a packed value directly to 705 | a file-like object. There is no additional behaviour. This function 706 | only exists for API parity with the :py:func:`unpack()` function. 707 | 708 | Except for the first argument (the file-like object), all arguments 709 | (both positional and keyword) are passed on to :py:func:`packb()`. 710 | See :py:func:`packb()` for more information. 711 | 712 | :param file-like fp: writeable file-like object 713 | :param args: propagated to :py:func:`packb()` 714 | :param kwargs: propagated to :py:func:`packb()` 715 | :return: number of bytes written 716 | :rtype: int 717 | """ 718 | return fp.write(packb(*args, **kwargs)) 719 | 720 | 721 | def unpackb(value): 722 | """ 723 | Unpack a *temporenc* value from a byte string. 724 | 725 | If no valid value could be read, this raises :py:exc:`ValueError`. 726 | 727 | :param bytes value: a byte string (or `bytearray`) to parse 728 | :return: a parsed *temporenc* structure 729 | :rtype: :py:class:`Moment` 730 | """ 731 | 732 | # 733 | # Unpack components 734 | # 735 | 736 | first = value[0] 737 | 738 | if PY2 and isinstance(first, bytes): # pragma: no cover 739 | first = ord(first) 740 | 741 | if PY26 and isinstance(value, bytearray): # pragma: no cover 742 | # struct.unpack() does not handle bytearray() in Python < 2.7 743 | value = bytes(value) 744 | 745 | type, precision, expected_length = _detect_type(first) 746 | 747 | if type is None: 748 | raise ValueError("first byte does not contain a valid tag") 749 | 750 | if len(value) != expected_length: 751 | if precision is None: 752 | raise ValueError( 753 | "{0} value must be {1:d} bytes; got {2:d}".format( 754 | type, expected_length, len(value))) 755 | else: 756 | raise ValueError( 757 | "{0} value with precision {1:02b} must be {2:d} bytes; " 758 | "got {3:d}".format( 759 | type, precision, expected_length, len(value))) 760 | 761 | date = time = tz_offset = nanosecond = padding = None 762 | 763 | if type == 'D': 764 | # 100DDDDD DDDDDDDD DDDDDDDD 765 | date = unpack_4(b'\x00' + value) & D_MASK 766 | 767 | elif type == 'T': 768 | # 1010000T TTTTTTTT TTTTTTTT 769 | time = unpack_4(b'\x00' + value) & T_MASK 770 | 771 | elif type == 'DT': 772 | # 00DDDDDD DDDDDDDD DDDDDDDT TTTTTTTT 773 | # TTTTTTTT 774 | n = unpack_8(b'\x00\x00\x00' + value) 775 | date = n >> 17 & D_MASK 776 | time = n & T_MASK 777 | 778 | elif type == 'DTZ': 779 | # 110DDDDD DDDDDDDD DDDDDDDD TTTTTTTT 780 | # TTTTTTTT TZZZZZZZ 781 | n = unpack_8(b'\x00\x00' + value) 782 | date = n >> 24 & D_MASK 783 | time = n >> 7 & T_MASK 784 | tz_offset = n & Z_MASK 785 | 786 | elif type == 'DTS': 787 | # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT 788 | # TTTTTTTT TT...... (first 6 bytes) 789 | n = unpack_8(b'\x00\x00' + value[:6]) >> 6 790 | date = n >> 17 & D_MASK 791 | time = n & T_MASK 792 | 793 | # Extract S component from last 4 bytes 794 | n = unpack_4(value[-4:]) 795 | if precision == 0b00: 796 | # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT 797 | # TTTTTTTT TTSSSSSS SSSS0000 798 | nanosecond = (n >> 4 & MILLISECOND_MASK) * 1000000 799 | padding = n & 0b1111 800 | elif precision == 0b01: 801 | # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT 802 | # TTTTTTTT TTSSSSSS SSSSSSSS SSSSSS00 803 | nanosecond = (n >> 2 & MICROSECOND_MASK) * 1000 804 | padding = n & 0b11 805 | elif precision == 0b10: 806 | # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT 807 | # TTTTTTTT TTSSSSSS SSSSSSSS SSSSSSSS 808 | # SSSSSSSS 809 | nanosecond = n & NANOSECOND_MASK 810 | elif precision == 0b11: 811 | # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT 812 | # TTTTTTTT TT000000 813 | padding = n & 0b111111 814 | 815 | elif type == 'DTSZ': 816 | # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT 817 | # TTTTTTTT TTT..... (first 6 bytes) 818 | n = unpack_8(b'\x00\x00' + value[:6]) >> 5 819 | date = n >> 17 & D_MASK 820 | time = n & T_MASK 821 | 822 | # Extract S and Z components from last 5 bytes 823 | n = unpack_8(b'\x00\x00\x00' + value[-5:]) 824 | if precision == 0b00: 825 | # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT 826 | # TTTTTTTT TTTSSSSS SSSSSZZZ ZZZZ0000 827 | nanosecond = (n >> 11 & MILLISECOND_MASK) * 1000000 828 | tz_offset = n >> 4 & Z_MASK 829 | padding = n & 0b1111 830 | elif precision == 0b01: 831 | # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT 832 | # TTTTTTTT TTTSSSSS SSSSSSSS SSSSSSSZ 833 | # ZZZZZZ00 834 | nanosecond = (n >> 9 & MICROSECOND_MASK) * 1000 835 | tz_offset = n >> 2 & Z_MASK 836 | padding = n & 0b11 837 | elif precision == 0b10: 838 | # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT 839 | # TTTTTTTT TTTSSSSS SSSSSSSS SSSSSSSS 840 | # SSSSSSSS SZZZZZZZ 841 | nanosecond = n >> 7 & NANOSECOND_MASK 842 | tz_offset = n & Z_MASK 843 | elif precision == 0b11: 844 | # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT 845 | # TTTTTTTT TTTZZZZZ ZZ000000 846 | tz_offset = n >> 6 & Z_MASK 847 | padding = n & 0b111111 848 | 849 | if padding: 850 | raise ValueError("padding bits must be zero") 851 | 852 | # 853 | # Split D and T components 854 | # 855 | 856 | if date is None: 857 | year = month = day = None 858 | else: 859 | year = date >> 9 & YEAR_MASK # always within range 860 | if year == YEAR_EMPTY: 861 | year = None 862 | 863 | month = date >> 5 & MONTH_MASK 864 | if month == MONTH_EMPTY: 865 | month = None 866 | elif month > MONTH_MAX: 867 | raise ValueError("month not within supported range") 868 | else: 869 | month += 1 870 | 871 | day = date & DAY_MASK # always within range 872 | if day == DAY_EMPTY: 873 | day = None 874 | else: 875 | day += 1 876 | 877 | if time is None: 878 | hour = minute = second = None 879 | else: 880 | hour = time >> 12 & HOUR_MASK 881 | if hour == HOUR_EMPTY: 882 | hour = None 883 | elif hour > HOUR_MAX: 884 | raise ValueError("hour not within supported range") 885 | 886 | minute = time >> 6 & MINUTE_MASK 887 | if minute == MINUTE_EMPTY: 888 | minute = None 889 | elif minute > MINUTE_MAX: 890 | raise ValueError("minute not within supported range") 891 | 892 | second = time & SECOND_MASK 893 | if second == SECOND_EMPTY: 894 | second = None 895 | elif second > SECOND_MAX: 896 | raise ValueError("second not within supported range") 897 | 898 | # 899 | # Normalize time zone offset 900 | # 901 | 902 | if tz_offset is not None: 903 | tz_offset = 15 * (tz_offset - 64) 904 | 905 | # 906 | # Sub-second fields are either all None, or none are None. 907 | # 908 | 909 | if nanosecond is not None and nanosecond > NANOSECOND_MAX: 910 | raise ValueError("sub-second precision not within supported range") 911 | 912 | return Moment( 913 | year, month, day, 914 | hour, minute, second, nanosecond, 915 | tz_offset) 916 | 917 | 918 | def unpack(fp): 919 | """ 920 | Unpack a *temporenc* value from a file-like object. 921 | 922 | This function consumes exactly the number of bytes required to 923 | unpack a single *temporenc* value. 924 | 925 | If no valid value could be read, this raises :py:exc:`ValueError`. 926 | 927 | :param file-like fp: readable file-like object 928 | :return: a parsed *temporenc* structure 929 | :rtype: :py:class:`Moment` 930 | """ 931 | first = fp.read(1) 932 | _, _, size = _detect_type(ord(first)) 933 | return unpackb(first + fp.read(size - 1)) 934 | -------------------------------------------------------------------------------- /tests/test_temporenc.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import datetime 3 | import io 4 | import operator 5 | import sys 6 | 7 | import pytest 8 | 9 | import temporenc 10 | 11 | PY2 = sys.version_info[0] == 2 12 | 13 | 14 | def from_hex(s): 15 | """Compatibility helper like bytes.fromhex() in Python 3""" 16 | return binascii.unhexlify(s.replace(' ', '').encode('ascii')) 17 | 18 | 19 | def test_type_d(): 20 | 21 | actual = temporenc.packb(type='D', year=1983, month=1, day=15) 22 | expected = from_hex('8f 7e 0e') 23 | assert actual == expected 24 | 25 | v = temporenc.unpackb(expected) 26 | assert (v.year, v.month, v.day) == (1983, 1, 15) 27 | assert (v.hour, v.minute, v.second) == (None, None, None) 28 | 29 | 30 | def test_type_t(): 31 | 32 | actual = temporenc.packb(type='T', hour=18, minute=25, second=12) 33 | expected = from_hex('a1 26 4c') 34 | assert actual == expected 35 | 36 | v = temporenc.unpackb(expected) 37 | assert (v.year, v.month, v.day) == (None, None, None) 38 | assert (v.hour, v.minute, v.second) == (18, 25, 12) 39 | 40 | 41 | def test_type_dt(): 42 | 43 | actual = temporenc.packb( 44 | type='DT', 45 | year=1983, month=1, day=15, 46 | hour=18, minute=25, second=12) 47 | expected = from_hex('1e fc 1d 26 4c') 48 | assert actual == expected 49 | 50 | v = temporenc.unpackb(expected) 51 | assert (v.year, v.month, v.day) == (1983, 1, 15) 52 | assert (v.hour, v.minute, v.second) == (18, 25, 12) 53 | 54 | 55 | def test_type_dtz(): 56 | 57 | actual = temporenc.packb( 58 | type='DTZ', 59 | year=1983, month=1, day=15, 60 | hour=18, minute=25, second=12, 61 | tz_offset=60) 62 | expected = from_hex('cf 7e 0e 93 26 44') 63 | assert actual == expected 64 | 65 | v = temporenc.unpackb(expected) 66 | assert (v.year, v.month, v.day) == (1983, 1, 15) 67 | assert (v.hour, v.minute, v.second) == (18, 25, 12) 68 | assert v.tz_offset == 60 69 | 70 | 71 | def test_type_dts(): 72 | 73 | actual = temporenc.packb( 74 | type='DTS', 75 | year=1983, month=1, day=15, 76 | hour=18, minute=25, second=12, millisecond=123) 77 | dts_ms = from_hex('47 bf 07 49 93 07 b0') 78 | assert actual == dts_ms 79 | v = temporenc.unpackb(dts_ms) 80 | assert (v.year, v.month, v.day) == (1983, 1, 15) 81 | assert (v.hour, v.minute, v.second) == (18, 25, 12) 82 | assert v.millisecond == 123 83 | assert v.microsecond == 123000 84 | assert v.nanosecond == 123000000 85 | 86 | actual = temporenc.packb( 87 | type='DTS', 88 | year=1983, month=1, day=15, 89 | hour=18, minute=25, second=12, microsecond=123456) 90 | dts_us = from_hex('57 bf 07 49 93 07 89 00') 91 | assert actual == dts_us 92 | v = temporenc.unpackb(dts_us) 93 | assert (v.year, v.month, v.day) == (1983, 1, 15) 94 | assert (v.hour, v.minute, v.second) == (18, 25, 12) 95 | assert v.millisecond == 123 96 | assert v.microsecond == 123456 97 | assert v.nanosecond == 123456000 98 | 99 | actual = temporenc.packb( 100 | type='DTS', 101 | year=1983, month=1, day=15, 102 | hour=18, minute=25, second=12, nanosecond=123456789) 103 | dts_ns = from_hex('67 bf 07 49 93 07 5b cd 15') 104 | assert actual == dts_ns 105 | v = temporenc.unpackb(dts_ns) 106 | assert (v.year, v.month, v.day) == (1983, 1, 15) 107 | assert (v.hour, v.minute, v.second) == (18, 25, 12) 108 | assert v.millisecond == 123 109 | assert v.microsecond == 123456 110 | assert v.nanosecond == 123456789 111 | 112 | actual = temporenc.packb( 113 | type='DTS', 114 | year=1983, month=1, day=15, 115 | hour=18, minute=25, second=12) 116 | dts_none = from_hex('77 bf 07 49 93 00') 117 | assert actual == dts_none 118 | v = temporenc.unpackb(dts_none) 119 | assert (v.year, v.month, v.day) == (1983, 1, 15) 120 | assert (v.hour, v.minute, v.second) == (18, 25, 12) 121 | assert v.millisecond is None 122 | assert v.microsecond is None 123 | assert v.nanosecond is None 124 | 125 | 126 | def test_type_dtsz(): 127 | 128 | actual = temporenc.packb( 129 | type='DTSZ', 130 | year=1983, month=1, day=15, 131 | hour=18, minute=25, second=12, millisecond=123, 132 | tz_offset=60) 133 | dtsz_ms = from_hex('e3 df 83 a4 c9 83 dc 40') 134 | assert actual == dtsz_ms 135 | v = temporenc.unpackb(dtsz_ms) 136 | assert (v.year, v.month, v.day) == (1983, 1, 15) 137 | assert (v.hour, v.minute, v.second) == (18, 25, 12) 138 | assert v.millisecond == 123 139 | assert v.microsecond == 123000 140 | assert v.nanosecond == 123000000 141 | assert v.tz_offset == 60 142 | 143 | actual = temporenc.packb( 144 | type='DTSZ', 145 | year=1983, month=1, day=15, 146 | hour=18, minute=25, second=12, microsecond=123456, 147 | tz_offset=60) 148 | dtsz_us = from_hex('eb df 83 a4 c9 83 c4 81 10') 149 | assert actual == dtsz_us 150 | assert temporenc.unpackb(dtsz_us).microsecond == 123456 151 | assert v.tz_offset == 60 152 | 153 | actual = temporenc.packb( 154 | type='DTSZ', 155 | year=1983, month=1, day=15, 156 | hour=18, minute=25, second=12, nanosecond=123456789, 157 | tz_offset=60) 158 | dtsz_ns = from_hex('f3 df 83 a4 c9 83 ad e6 8a c4') 159 | assert actual == dtsz_ns 160 | assert temporenc.unpackb(dtsz_ns).nanosecond == 123456789 161 | assert v.tz_offset == 60 162 | 163 | actual = temporenc.packb( 164 | type='DTSZ', 165 | year=1983, month=1, day=15, 166 | hour=18, minute=25, second=12, 167 | tz_offset=60) 168 | dtsz_none = from_hex('fb df 83 a4 c9 91 00') 169 | assert actual == dtsz_none 170 | v = temporenc.unpackb(dtsz_none) 171 | assert v.millisecond is None 172 | assert v.millisecond is None 173 | assert v.millisecond is None 174 | assert v.tz_offset == 60 175 | 176 | 177 | def test_type_detection(): 178 | 179 | # Empty value, so should result in the smallest type 180 | assert len(temporenc.packb()) == 3 181 | 182 | # Type D 183 | assert len(temporenc.packb(year=1983)) == 3 184 | assert temporenc.unpackb(temporenc.packb(year=1983)).year == 1983 185 | 186 | # Type T 187 | assert len(temporenc.packb(hour=18)) == 3 188 | assert temporenc.unpackb(temporenc.packb(hour=18)).hour == 18 189 | 190 | # Type DT 191 | assert len(temporenc.packb(year=1983, hour=18)) == 5 192 | 193 | # Type DTS 194 | assert len(temporenc.packb(millisecond=0)) == 7 195 | assert len(temporenc.packb(microsecond=0)) == 8 196 | assert len(temporenc.packb(nanosecond=0)) == 9 197 | 198 | # Type DTZ 199 | assert len(temporenc.packb(year=1983, hour=18, tz_offset=120)) == 6 200 | 201 | # Type DTSZ 202 | assert len(temporenc.packb(millisecond=0, tz_offset=120)) == 8 203 | 204 | 205 | def test_type_empty_values(): 206 | v = temporenc.unpackb(temporenc.packb(type='DTS')) 207 | assert (v.year, v.month, v.day) == (None, None, None) 208 | assert (v.hour, v.minute, v.second) == (None, None, None) 209 | assert (v.millisecond, v.microsecond, v.nanosecond) == (None, None, None) 210 | assert v.tz_offset is None 211 | 212 | 213 | def test_incorrect_sizes(): 214 | 215 | # Too long 216 | with pytest.raises(ValueError): 217 | temporenc.unpackb(temporenc.packb(year=1983) + b'foo') 218 | with pytest.raises(ValueError): 219 | temporenc.unpackb(temporenc.packb(millisecond=0) + b'foo') 220 | 221 | # Too short 222 | with pytest.raises(ValueError): 223 | temporenc.unpackb(temporenc.packb(year=1983)[:-1]) 224 | with pytest.raises(ValueError): 225 | temporenc.unpackb(temporenc.packb(millisecond=0)[:-1]) 226 | 227 | 228 | def test_unpack_bytearray(): 229 | ba = bytearray((0x8f, 0x7e, 0x0e)) 230 | assert temporenc.unpackb(ba) is not None 231 | 232 | 233 | def test_stream_unpacking(): 234 | # This stream contains two values and one byte of trailing data 235 | fp = io.BytesIO(from_hex('8f 7e 0e 8f 7e 0f ff')) 236 | assert temporenc.unpack(fp).day == 15 237 | assert fp.tell() == 3 238 | assert temporenc.unpack(fp).day == 16 239 | assert fp.tell() == 6 240 | assert fp.read() == b'\xff' 241 | 242 | 243 | def test_stream_packing(): 244 | fp = io.BytesIO() 245 | assert temporenc.pack(fp, year=1983) == 3 246 | assert temporenc.pack(fp, year=1984) == 3 247 | assert fp.tell() == 6 248 | assert len(fp.getvalue()) == 6 249 | 250 | 251 | def test_wrong_type(): 252 | with pytest.raises(ValueError): 253 | temporenc.packb(type="foo", year=1983) 254 | 255 | 256 | def test_out_of_range_values(): 257 | with pytest.raises(ValueError): 258 | temporenc.packb(year=123456) 259 | 260 | with pytest.raises(ValueError): 261 | temporenc.packb(month=-12) 262 | 263 | with pytest.raises(ValueError): 264 | temporenc.packb(day=1234) 265 | 266 | with pytest.raises(ValueError): 267 | temporenc.packb(hour=1234) 268 | 269 | with pytest.raises(ValueError): 270 | temporenc.packb(minute=1234) 271 | 272 | with pytest.raises(ValueError): 273 | temporenc.packb(second=1234) 274 | 275 | with pytest.raises(ValueError): 276 | temporenc.packb(millisecond=1000) 277 | 278 | with pytest.raises(ValueError): 279 | temporenc.packb(microsecond=1000000) 280 | 281 | with pytest.raises(ValueError): 282 | temporenc.packb(nanosecond=10000000000) 283 | 284 | with pytest.raises(ValueError): 285 | temporenc.packb(tz_offset=1050) 286 | 287 | with pytest.raises(ValueError): 288 | temporenc.packb(tz_offset=13) # not a full quarter 289 | 290 | 291 | def test_unpacking_bogus_data(): 292 | with pytest.raises(ValueError) as e: 293 | # First byte can never occur in valid values. 294 | temporenc.unpackb(from_hex('bb 12 34')) 295 | assert 'tag' in str(e.value) 296 | 297 | with pytest.raises(ValueError) as e: 298 | temporenc.unpackb(from_hex('47 bf 07 49 93 07 b2')) 299 | assert 'padding' in str(e.value) 300 | 301 | 302 | def test_range_check_unpacking(): 303 | 304 | # Type T with out of range hour 305 | with pytest.raises(ValueError) as e: 306 | temporenc.unpackb(bytearray(( 307 | 0b10100001, 0b11100000, 0b00000000))) 308 | assert 'hour' in str(e.value) 309 | 310 | # Type T with out of range minute 311 | with pytest.raises(ValueError) as e: 312 | temporenc.unpackb(bytearray(( 313 | 0b10100000, 0b00001111, 0b01000000))) 314 | assert 'minute' in str(e.value) 315 | 316 | # Type T with out of range second 317 | with pytest.raises(ValueError) as e: 318 | temporenc.unpackb(bytearray(( 319 | 0b10100000, 0b00000000, 0b00111110))) 320 | assert 'second' in str(e.value) 321 | 322 | # Type D with out of range month 323 | with pytest.raises(ValueError) as e: 324 | temporenc.unpackb(bytearray(( 325 | 0b10000000, 0b00000001, 0b11000000))) 326 | assert 'month' in str(e.value) 327 | 328 | # Type DTS with out of range millisecond 329 | with pytest.raises(ValueError) as e: 330 | temporenc.unpackb(bytearray(( 331 | 0b01000000, 0b00000000, 0b00000000, 0b00000000, 332 | 0b00000000, 0b00111111, 0b11110000))) 333 | assert 'sub-second' in str(e.value) 334 | 335 | # Type DTS with out of range microsecond 336 | with pytest.raises(ValueError) as e: 337 | temporenc.unpackb(bytearray(( 338 | 0b01010000, 0b00000000, 0b00000000, 0b00000000, 339 | 0b00000000, 0b00111111, 0b11111111, 0b11111100))) 340 | assert 'sub-second' in str(e.value) 341 | 342 | # Type DTS with out of range nanosecond 343 | with pytest.raises(ValueError) as e: 344 | temporenc.unpackb(bytearray(( 345 | 0b01100000, 0b00000000, 0b00000000, 0b00000000, 346 | 0b00000000, 0b00111111, 0b11111111, 0b11111111, 347 | 0b11111111))) 348 | assert 'sub-second' in str(e.value) 349 | 350 | 351 | def test_native_packing(): 352 | 353 | with pytest.raises(ValueError): 354 | temporenc.packb(object()) 355 | 356 | # datetime.date => D 357 | actual = temporenc.packb(datetime.date(1983, 1, 15)) 358 | expected = from_hex('8f 7e 0e') 359 | assert actual == expected 360 | 361 | # datetime.datetime => DTS, unless told otherwise 362 | actual = temporenc.packb(datetime.datetime( 363 | 1983, 1, 15, 18, 25, 12, 123456)) 364 | expected = from_hex('57 bf 07 49 93 07 89 00') 365 | assert actual == expected 366 | 367 | actual = temporenc.packb( 368 | datetime.datetime(1983, 1, 15, 18, 25, 12), 369 | type='DT') 370 | expected = from_hex('1e fc 1d 26 4c') 371 | assert actual == expected 372 | 373 | # datetime.time => DTS, unless told otherwise 374 | assert len(temporenc.packb(datetime.datetime.now().time())) == 8 375 | actual = temporenc.packb( 376 | datetime.time(18, 25, 12), 377 | type='T') 378 | expected = from_hex('a1 26 4c') 379 | assert actual == expected 380 | 381 | 382 | def test_native_packing_with_overrides(): 383 | actual = temporenc.packb( 384 | datetime.datetime(1984, 1, 16, 18, 26, 12, 123456), 385 | year=1983, day=15, minute=25) 386 | expected = from_hex('57 bf 07 49 93 07 89 00') 387 | assert actual == expected 388 | 389 | 390 | def test_native_unpacking(): 391 | value = temporenc.unpackb(temporenc.packb( 392 | year=1983, month=1, day=15)) 393 | assert value.date() == datetime.date(1983, 1, 15) 394 | 395 | value = temporenc.unpackb(temporenc.packb( 396 | year=1983, month=1, day=15, 397 | hour=1, minute=2, second=3, microsecond=456)) 398 | assert value.datetime() == datetime.datetime(1983, 1, 15, 1, 2, 3, 456) 399 | 400 | value = temporenc.unpackb(temporenc.packb( 401 | year=1983, month=1, day=15, # will be ignored 402 | hour=1, minute=2, second=3, microsecond=456)) 403 | assert value.time() == datetime.time(1, 2, 3, 456) 404 | 405 | value = temporenc.unpackb(temporenc.packb(year=1234)) 406 | with pytest.raises(ValueError): 407 | value.date() 408 | assert value.date(strict=False).year == 1234 409 | assert value.datetime(strict=False).year == 1234 410 | 411 | value = temporenc.unpackb(temporenc.packb(hour=14)) 412 | with pytest.raises(ValueError): 413 | value.time() 414 | assert value.time(strict=False).hour == 14 415 | assert value.datetime(strict=False).hour == 14 416 | 417 | 418 | def test_native_unpacking_leap_second(): 419 | value = temporenc.unpackb(temporenc.packb( 420 | year=2013, month=6, day=30, 421 | hour=23, minute=59, second=60)) 422 | 423 | with pytest.raises(ValueError): 424 | value.datetime() # second out of range 425 | 426 | dt = value.datetime(strict=False) 427 | assert dt == datetime.datetime(2013, 6, 30, 23, 59, 59) 428 | 429 | 430 | def test_native_unpacking_incomplete(): 431 | moment = temporenc.unpackb(temporenc.packb(type='DT', year=1983, hour=12)) 432 | with pytest.raises(ValueError): 433 | moment.date() 434 | with pytest.raises(ValueError): 435 | moment.time() 436 | with pytest.raises(ValueError): 437 | moment.datetime() 438 | 439 | moment = temporenc.unpackb(temporenc.packb(datetime.datetime.now().date())) 440 | with pytest.raises(ValueError): 441 | moment.datetime() 442 | 443 | 444 | def test_native_time_zone(): 445 | 446 | # Python < 3.2 doesn't have concrete tzinfo implementations. This 447 | # test uses the internal helper class instead to avoid depending on 448 | # newer Python versions (or on pytz). 449 | from temporenc.temporenc import FixedOffset 450 | 451 | dutch_winter = FixedOffset(60) # UTC +01:00 452 | zero_delta = datetime.timedelta(0) 453 | hour_delta = datetime.timedelta(minutes=60) 454 | 455 | expected_name = "UTC+01:00" 456 | assert dutch_winter.tzname(None) == expected_name 457 | assert expected_name in str(dutch_winter) 458 | assert expected_name in repr(dutch_winter) 459 | assert dutch_winter.dst(None) == zero_delta 460 | 461 | # DTZ 462 | actual = temporenc.packb( 463 | datetime.datetime(1983, 1, 15, 18, 25, 12, 0, tzinfo=dutch_winter), 464 | type='DTZ') 465 | expected = from_hex('cf 7e 0e 93 26 44') 466 | assert actual == expected 467 | moment = temporenc.unpackb(expected) 468 | assert moment.hour == 18 469 | assert moment.tz_offset == 60 470 | dt = moment.datetime() 471 | assert dt.hour == 18 472 | assert dt.utcoffset() == hour_delta 473 | 474 | # DTSZ (microsecond, since native types have that precision) 475 | actual = temporenc.packb( 476 | datetime.datetime( 477 | 1983, 1, 15, 18, 25, 12, 123456, 478 | tzinfo=dutch_winter), 479 | type='DTSZ') 480 | dtsz_us = from_hex('eb df 83 a4 c9 83 c4 81 10') 481 | assert actual == dtsz_us 482 | moment = temporenc.unpackb(expected) 483 | assert moment.datetime().hour == 18 484 | 485 | # Time only with time zone 486 | moment = temporenc.unpackb(temporenc.packb( 487 | datetime.time(0, 30, 0, 123456, tzinfo=dutch_winter), 488 | type='DTSZ')) 489 | assert moment.tz_offset == 60 490 | for obj in [moment.time(), moment.datetime(strict=False)]: 491 | assert (obj.hour, obj.minute, obj.microsecond) == (0, 30, 123456) 492 | assert obj.utcoffset() == hour_delta 493 | 494 | 495 | def test_string_conversion(): 496 | 497 | # Date only 498 | value = temporenc.unpackb(temporenc.packb(year=1983, month=1, day=15)) 499 | assert str(value) == "1983-01-15" 500 | value = temporenc.unpackb(temporenc.packb(year=1983, day=15)) 501 | assert str(value) == "1983-??-15" 502 | 503 | # Time only 504 | value = temporenc.unpackb(temporenc.packb(hour=1, minute=2, second=3)) 505 | assert str(value) == "01:02:03" 506 | value = temporenc.unpackb(temporenc.packb( 507 | hour=1, second=3, microsecond=12340)) 508 | assert str(value) == "01:??:03.01234" 509 | 510 | # Date and time 511 | value = temporenc.unpackb(temporenc.packb( 512 | year=1983, month=1, day=15, 513 | hour=18, minute=25)) 514 | assert str(value) == "1983-01-15 18:25:??" 515 | 516 | # If sub-second is set but equal to 0, the string should show it 517 | # properly anyway. 518 | value = temporenc.unpackb(temporenc.packb( 519 | hour=12, minute=34, second=56, microsecond=0)) 520 | assert str(value) == "12:34:56.0" 521 | 522 | # Time zone info should be included 523 | moment = temporenc.unpackb(from_hex('cf 7e 0e 93 26 40')) 524 | assert str(moment) == '1983-01-15 18:25:12Z' 525 | moment = temporenc.unpackb(from_hex('cf 7e 0e 93 26 44')) 526 | assert str(moment) == '1983-01-15 18:25:12+01:00' 527 | 528 | # Very contrived example... 529 | value = temporenc.unpackb(temporenc.packb(microsecond=1250)) 530 | assert str(value) == "??:??:??.00125" 531 | 532 | # And a basic one for repr() 533 | value = temporenc.unpackb(temporenc.packb(hour=12, minute=34, second=56)) 534 | assert '12:34:56' in repr(value) 535 | 536 | 537 | def test_comparison(): 538 | now = datetime.datetime.now() 539 | later = now.replace(microsecond=0) + datetime.timedelta(hours=1) 540 | v1 = temporenc.unpackb(temporenc.packb(now)) 541 | v2 = temporenc.unpackb(temporenc.packb(now)) 542 | v3 = temporenc.unpackb(temporenc.packb(later)) 543 | 544 | # Same 545 | assert v1 == v2 546 | assert v1 != v3 547 | assert v1 >= v2 548 | assert v1 >= v2 549 | assert not (v1 > v2) 550 | assert not (v1 < v2) 551 | 552 | # Different 553 | assert v3 > v1 554 | assert v1 < v3 555 | assert v3 >= v1 556 | assert v1 <= v3 557 | 558 | # Equality tests against other types: not equal 559 | bogus = 'junk' 560 | assert not (v1 == bogus) 561 | assert v1 != bogus 562 | 563 | # Comparison against other types: 564 | # * fail on Python 3 (unorderable types) 565 | # * use fallback comparison on Python 2. 566 | for op in (operator.gt, operator.lt, operator.ge, operator.le): 567 | if PY2: 568 | op(v1, bogus) # should not raise 569 | else: 570 | with pytest.raises(TypeError): 571 | op(v1, bogus) # should raise 572 | 573 | 574 | def test_hash(): 575 | 576 | now = datetime.datetime.now() 577 | later = now.replace(microsecond=0) + datetime.timedelta(hours=1) 578 | v1 = temporenc.unpackb(temporenc.packb(now)) 579 | v2 = temporenc.unpackb(temporenc.packb(now)) 580 | v3 = temporenc.unpackb(temporenc.packb(later)) 581 | 582 | assert hash(v1) == hash(v2) 583 | assert hash(v1) != hash(v3) 584 | 585 | d = {} 586 | d[v1] = 1 587 | d[v2] = 2 588 | d[v3] = 3 589 | assert len(d) == 2 590 | assert d[v1] == 2 591 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,py35,py36 3 | 4 | [testenv] 5 | deps=-rrequirements-test.txt 6 | commands=py.test tests/ 7 | --------------------------------------------------------------------------------