|
1 | 1 | # encoding: utf-8
|
2 | 2 |
|
| 3 | +"""Marrow Mongo Date field specialization. |
| 4 | +
|
| 5 | +Commentary on high-level management of timezone casting: |
| 6 | +
|
| 7 | + https://groups.google.com/forum/#!topic/mongodb-user/GOMjTJON4cg |
| 8 | +""" |
| 9 | + |
3 | 10 | from __future__ import unicode_literals
|
4 | 11 |
|
5 |
| -from bson import ObjectId |
| 12 | +from datetime import datetime, timedelta, tzinfo |
| 13 | +from bson import ObjectId as OID |
| 14 | +from collections import MutableMapping |
| 15 | +from datetime import datetime, timedelta |
6 | 16 |
|
7 | 17 | from .base import Field
|
| 18 | +from ...util import utc, utcnow |
| 19 | +from ....schema import Attribute |
| 20 | + |
| 21 | +# Conditional dependencies. |
| 22 | + |
| 23 | +try: |
| 24 | + from pytz import timezone as get_tz |
| 25 | +except ImportError: |
| 26 | + get_tz = None |
| 27 | + |
| 28 | +try: |
| 29 | + localtz = __import__('tzlocal').get_localzone() |
| 30 | +except ImportError: |
| 31 | + localtz = None |
| 32 | + |
| 33 | + |
| 34 | +log = __import__('logging').getLogger(__name__) |
8 | 35 |
|
9 | 36 |
|
10 | 37 | class Date(Field):
|
| 38 | + """MongoDB date/time storage. |
| 39 | + |
| 40 | + Accepts the following options in addition to the base Field options: |
| 41 | + |
| 42 | + `naive`: The timezone to interpret assigned "naive" datetime objects as. |
| 43 | + `timezone`: The timezone to cast objects retrieved from the database to. |
| 44 | + |
| 45 | + Timezone references may be, or may be a callback returning, a `tzinfo`-suitable object, the string name of a |
| 46 | + timezone according to `pytz`, the alias 'naive' (strip or ignore the timezone) or 'local' (the local host's) |
| 47 | + timezone explicitly. None values imply no conversion. |
| 48 | + |
| 49 | + All dates are converted to and stored in UTC for storage within MongoDB; the original timezone is lost. As a |
| 50 | + result if `naive` is `None` then assignment of naive `datetime` objects will fail. |
| 51 | + """ |
| 52 | + |
11 | 53 | __foreign__ = 'date'
|
12 | 54 | __disallowed_operators__ = {'#array'}
|
13 | 55 |
|
| 56 | + naive = Attribute(default=utc) |
| 57 | + tz = Attribute(default=None) # Timezone to cast to when retrieving from the database. |
| 58 | + |
| 59 | + def _process_tz(self, dt, naive, tz): |
| 60 | + """Process timezone casting and conversion.""" |
| 61 | + |
| 62 | + def _tz(t): |
| 63 | + if t is None: |
| 64 | + return |
| 65 | + |
| 66 | + if t == 'local': |
| 67 | + if __debug__ and not localtz: |
| 68 | + raise ValueError("Requested conversion to local timezone, but `localtz` not installed.") |
| 69 | + |
| 70 | + t = localtz |
| 71 | + |
| 72 | + if not isinstance(t, tzinfo): |
| 73 | + t = get_tz(t) |
| 74 | + |
| 75 | + if not hasattr(t, 'normalize'): # Attempt to handle non-pytz tzinfo. |
| 76 | + t = get_tz(t.tzname()) |
| 77 | + |
| 78 | + return t |
| 79 | + |
| 80 | + naive = _tz(naive) |
| 81 | + tz = _tz(tz) |
| 82 | + |
| 83 | + if not dt.tzinfo and naive: |
| 84 | + dt = naive.localize(dt) |
| 85 | + |
| 86 | + if tz: |
| 87 | + dt = tz.normalize(dt.astimezone(tz)) |
| 88 | + |
| 89 | + return dt |
| 90 | + |
| 91 | + def to_native(self, obj, name, value): |
| 92 | + if not isinstance(value, datetime): |
| 93 | + log.warn("Non-date stored in {}.{} field.".format(self.__class__.__name__, ~self), |
| 94 | + extra={'document': obj, 'field': ~self, 'value': value}) |
| 95 | + return value |
| 96 | + |
| 97 | + return self._process_tz(value, self.naive, self.tz) |
| 98 | + |
14 | 99 | def to_foreign(self, obj, name, value): # pylint:disable=unused-argument
|
15 |
| - if isinstance(value, ObjectId): |
16 |
| - return value.generation_time |
| 100 | + if isinstance(value, MutableMapping) and '_id' in value: |
| 101 | + value = value['_id'] |
| 102 | + |
| 103 | + if isinstance(value, OID): |
| 104 | + value = value.generation_time |
| 105 | + |
| 106 | + elif isinstance(value, timedelta): |
| 107 | + value = utcnow() + value |
| 108 | + |
| 109 | + assert isinstance(value, datetime), "Value must be a datetime, ObjectId, or identified document, not: " + \ |
| 110 | + repr(value) |
17 | 111 |
|
18 |
| - return value |
| 112 | + return self._process_tz(value, self.naive, utc) |
0 commit comments