Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] fields.DateTime timestamp, timestamp_ms, timezone, timezone_naive added #1003

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions marshmallow/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,13 +932,17 @@ class DateTime(Field):
'iso8601': utils.isoformat,
'rfc': utils.rfcformat,
'rfc822': utils.rfcformat,
'timestamp': utils.to_timestamp,
'timestamp_ms': utils.to_timestamp_ms,
}

DESERIALIZATION_FUNCS = {
'iso': utils.from_iso_datetime,
'iso8601': utils.from_iso_datetime,
'rfc': utils.from_rfc,
'rfc822': utils.from_rfc,
'timestamp': utils.from_timestamp,
'timestamp_ms': utils.from_timestamp_ms,
}

DEFAULT_FORMAT = 'iso'
Expand All @@ -953,12 +957,14 @@ class DateTime(Field):
'format': '"{input}" cannot be formatted as a {obj_type}.',
}

def __init__(self, format=None, **kwargs):
def __init__(self, format=None, timezone=None, timezone_naive=False, **kwargs):
super(DateTime, self).__init__(**kwargs)
# Allow this to be None. It may be set later in the ``_serialize``
# Allow format to be None. It may be set later in the ``_serialize``
# or ``_deserialize`` methods This allows a Schema to dynamically set the
# format, e.g. from a Meta option
self.format = format
self.timezone = timezone # TODO: add str conversion if isinstance(timezone, basestring)
self.timezone_naive = timezone_naive

def _bind_to_schema(self, field_name, schema):
super(DateTime, self)._bind_to_schema(field_name, schema)
Expand All @@ -971,6 +977,15 @@ def _bind_to_schema(self, field_name, schema):
def _serialize(self, value, attr, obj):
if value is None:
return None

if self.timezone:
if value.tzinfo is None:
value = value.replace(tzinfo=self.timezone)
if self.format in ('timestamp', 'timestamp_ms'):
# We're replacing to UTC to prevent to_timestamp from utc_offset conversion
# in case timezone is not UTC
value = value.astimezone(self.timezone).replace(tzinfo=utils.UTC)

data_format = self.format or self.DEFAULT_FORMAT
format_func = self.SERIALIZATION_FUNCS.get(data_format)
if format_func:
Expand All @@ -981,21 +996,32 @@ def _serialize(self, value, attr, obj):
else:
return value.strftime(data_format)

def _deserialize(self, value, attr, data):
if not value: # Falsy values, e.g. '', None, [] are not valid
raise self.fail('invalid', obj_type=self.OBJ_TYPE)
def _deserialize_dt(self, value, attr, data):
data_format = self.format or self.DEFAULT_FORMAT
func = self.DESERIALIZATION_FUNCS.get(data_format)
if func:
try:
return func(value)
except (TypeError, AttributeError, ValueError):
raise self.fail('invalid', obj_type=self.OBJ_TYPE)
self.fail('invalid', obj_type=self.OBJ_TYPE)
else:
try:
return self._make_object_from_format(value, data_format)
except (TypeError, AttributeError, ValueError):
raise self.fail('invalid', obj_type=self.OBJ_TYPE)
self.fail('invalid', obj_type=self.OBJ_TYPE)

def _deserialize(self, value, attr, data):
if not value and value != 0: # Falsy values, e.g. '', None, [] are not valid
self.fail('invalid', obj_type=self.OBJ_TYPE)
dt = self._deserialize_dt(value, attr, data)

if self.timezone and (dt.tzinfo is None or self.format in ('timestamp', 'timestamp_ms')):
dt = dt.replace(tzinfo=self.timezone)
if self.timezone_naive:
if self.timezone and value.tzinfo is not None:
return dt.astimezone(self.timezone).replace(tzinfo=None)
return dt.replace(tzinfo=None)
return dt

@staticmethod
def _make_object_from_format(value, data_format):
Expand Down
27 changes: 27 additions & 0 deletions marshmallow/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,38 @@ def to_iso_date(date, *args, **kwargs):
return datetime.date.isoformat(date)


def from_timestamp(timestamp, tzinfo=UTC):
return datetime.fromutctimestamp(float(timestamp)).replace(tzinfo=tzinfo)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the documentation, datetime.fromtimestamp(value, tz=datetime.timezone.utc) is preferred over datetime.fromutctimestamp(value).



def to_timestamp(dt, localtime=False, utc_offset=True):
"""Converts to timestamp, preserves offset if utc_offset=False"""
# TODO: do we need localtime?
if not dt.tzinfo or not utc_offset:
# We must ensure datetime is timezone-aware,
# because it will be converted with local time offset otherwise.
# If utc_offset is False - datetime will be converted "as is",
# offset related to timezone will be added otherwise
return dt.replace(tzinfo=UTC).timestamp()
# If we're not replacing tzinfo to UTC, timestamp will be converted
# with utc offset by default
return dt.timestamp()


def from_timestamp_ms(value, tzinfo=UTC):
return from_timestamp(float(value) / 1000, tzinfo)


def to_timestamp_ms(value, localtime=False, utc_offset=True):
return to_timestamp(value, localtime, utc_offset) * 1000


def ensure_text_type(val):
if isinstance(val, binary_type):
val = val.decode('utf-8')
return text_type(val)


def pluck(dictlist, key):
"""Extracts a list of dictionary values from a list of dictionaries.
::
Expand Down