Skip to content

Commit c0cb7d2

Browse files
committed
Addition of timezone handling to Date fields.
1 parent 80ea953 commit c0cb7d2

File tree

1 file changed

+98
-4
lines changed

1 file changed

+98
-4
lines changed

marrow/mongo/core/field/date.py

+98-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,112 @@
11
# encoding: utf-8
22

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+
310
from __future__ import unicode_literals
411

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
616

717
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__)
835

936

1037
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+
1153
__foreign__ = 'date'
1254
__disallowed_operators__ = {'#array'}
1355

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+
1499
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)
17111

18-
return value
112+
return self._process_tz(value, self.naive, utc)

0 commit comments

Comments
 (0)