Skip to content
This repository has been archived by the owner on Apr 10, 2023. It is now read-only.

Add DateTime, Date, and Time fields #29

Merged
merged 6 commits into from
Nov 21, 2018
Merged
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
11 changes: 11 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
=========

Version 0.2.1
-------------

*Unreleased*

- Add DateTime, Date, and Time Fields. (`#2`_, `#29`_)

.. _#29: /~https://github.com/rossmacarthur/serde/pull/29

.. _#2: /~https://github.com/rossmacarthur/serde/issues/2

Version 0.2.0
-------------

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
metadata['long_description'] = f.read()

install_requires = [
'isodate>=0.6.0<0.7.0',
'validators>=0.12.0<0.13.0'
]

Expand Down
11 changes: 6 additions & 5 deletions src/serde/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,17 @@

from .error import DeserializationError, SerdeError, SerializationError, ValidationError
from .field import (
Bool, Boolean, Choice, Dict, Dictionary, Domain, Email, Field, Float,
Instance, Int, Integer, List, Nested, Slug, Str, String, Tuple, Url, Uuid
Bool, Boolean, Choice, Date, DateTime, Dict, Dictionary, Domain, Email, Field, Float,
Instance, Int, Integer, List, Nested, Slug, Str, String, Time, Tuple, Url, Uuid
)
from .model import Model


__all__ = [
'Bool', 'Boolean', 'Choice', 'DeserializationError', 'Dict', 'Dictionary', 'Domain', 'Email',
'Field', 'Float', 'Instance', 'Int', 'Integer', 'List', 'Model', 'Nested', 'SerdeError',
'SerializationError', 'Slug', 'Str', 'String', 'Tuple', 'Url', 'Uuid', 'ValidationError'
'Bool', 'Boolean', 'Choice', 'Date', 'DateTime', 'DeserializationError', 'Dict', 'Dictionary',
'Domain', 'Email', 'Field', 'Float', 'Instance', 'Int', 'Integer', 'List', 'Model', 'Nested',
'SerdeError', 'SerializationError', 'Slug', 'Str', 'String', 'Time', 'Tuple', 'Url', 'Uuid',
'ValidationError'
]
__title__ = 'serde'
__version__ = '0.2.0'
Expand Down
124 changes: 121 additions & 3 deletions src/serde/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,20 @@
OrderedDict([('sILLy', 'tset')])
"""

import datetime
import uuid

import isodate

from . import validate
from .error import SerdeError
from .util import zip_equal


__all__ = [
'Bool', 'Boolean', 'Choice', 'Dict', 'Dictionary', 'Domain', 'Email', 'Field', 'Float',
'Instance', 'Int', 'Integer', 'List', 'Nested', 'Slug', 'Str', 'String', 'Tuple', 'Url', 'Uuid',
'create'
'Bool', 'Boolean', 'Choice', 'Date', 'DateTime', 'Dict', 'Dictionary', 'Domain', 'Email',
'Field', 'Float', 'Instance', 'Int', 'Integer', 'List', 'Nested', 'Slug', 'Str', 'String',
'Time', 'Tuple', 'Url', 'Uuid', 'create'
]


Expand Down Expand Up @@ -1071,6 +1074,121 @@ def validate(self, value):
validate.contains(self.choices)(value)


class DateTime(Instance):
"""
A `~datetime.datetime` field.

This field serializes `~datetime.datetime` objects as strings and
deserializes string representations of datetimes as `~datetime.datetime`
objects.

The date format can be specified. It will default to ISO 8601.

::

>>> class Entry(Model):
... timestamp = DateTime(format='%Y-%m-%d %H:%M:%S')

>>> entry = Entry(datetime.datetime(2001, 9, 11, 12, 5, 48))
>>> entry.to_dict()
OrderedDict([('timestamp', '2001-09-11 12:05:48')])
"""

type = datetime.datetime

def __init__(self, format='iso8601', **kwargs):
"""
Create a new DateTime.

Args:
format (str): the datetime format to use. "iso8601" may be used for
ISO 8601 datetimes.
**kwargs: keyword arguments for the `Field` constructor.
"""
super().__init__(self.__class__.type, **kwargs)
self.format = format

def serialize(self, value):
"""
Serialize the given `~datetime.datetime` as a string.

Args:
value (~datetime.datetime): the datetime object to serialize.

Returns:
str: a string representation of the datetime.
"""
if self.format == 'iso8601':
return value.isoformat()
else:
return value.strftime(self.format)

def deserialize(self, value):
"""
Deserialize the given string as a `~datetime.datetime`.

Args:
value (str): the string to deserialize.

Returns:
~datetime.datetime: the deserialized datetime.
"""
if self.format == 'iso8601':
return isodate.parse_datetime(value)
else:
return datetime.datetime.strptime(value, self.format)


class Date(DateTime):
"""
A `~datetime.date` field.

This field behaves in a similar fashion to the `DateTime` field.
"""

type = datetime.date

def deserialize(self, value):
"""
Deserialize the given string as a `~datetime.date`.

Args:
value (str): the string to deserialize.

Returns:
~datetime.date: the deserialized date.
"""
if self.format == 'iso8601':
return isodate.parse_date(value)
else:
return datetime.datetime.strptime(value, self.format).date()


class Time(DateTime):
"""
A `~datetime.time` field.

This field behaves in a similar fashion to the `DateTime` field.
"""

type = datetime.time

def deserialize(self, value):
"""
Deserialize the given string as a `~datetime.time`.

Args:
value (str): the string to deserialize.

Returns:
~datetime.time: the deserialized date.
"""
if self.format == 'iso8601':
return isodate.parse_time(value)
else:
return datetime.datetime.strptime(value, self.format).time()


class Uuid(Instance):
"""
A `~uuid.UUID` field.
Expand Down
61 changes: 59 additions & 2 deletions tests/test_field.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import datetime
import uuid
from collections import OrderedDict

from pytest import raises

from serde import (
Bool, Choice, Dict, Domain, Email, Field, Float, Instance, Int, List,
Model, Nested, SerdeError, Slug, Str, Tuple, Url, Uuid, ValidationError
Bool, Choice, Date, DateTime, Dict, Domain, Email, Field, Float, Instance, Int,
List, Model, Nested, SerdeError, Slug, Str, Time, Tuple, Url, Uuid, ValidationError
)
from serde.field import create, resolve_to_field_instance

Expand Down Expand Up @@ -410,6 +411,62 @@ def test_validate(self):
field.validate('test')


class TestDateTime:

def test___init__(self):
field = DateTime(format='%Y%m%d %H:%M:%S', required=False)
assert field.required is False
assert field.format == '%Y%m%d %H:%M:%S'

def test_serialize(self):
field = DateTime()
assert field.serialize(datetime.datetime(2001, 9, 11, 12, 5, 48)) == '2001-09-11T12:05:48'

field = DateTime(format='%Y%m%d %H:%M:%S')
assert field.serialize(datetime.datetime(2001, 9, 11, 12, 5, 48)) == '20010911 12:05:48'

def test_deserialize(self):
field = DateTime()
assert field.deserialize('2001-09-11T12:05:48') == datetime.datetime(2001, 9, 11, 12, 5, 48)

field = DateTime(format='%Y%m%d %H:%M:%S')
assert field.deserialize('20010911 12:05:48') == datetime.datetime(2001, 9, 11, 12, 5, 48)


class TestDate:

def test_serialize(self):
field = Date()
assert field.serialize(datetime.date(2001, 9, 11)) == '2001-09-11'

field = Date(format='%Y%m%d')
assert field.serialize(datetime.date(2001, 9, 11)) == '20010911'

def test_deserialize(self):
field = Date()
assert field.deserialize('2001-09-11') == datetime.date(2001, 9, 11)

field = Date(format='%Y%m%d')
assert field.deserialize('20010911') == datetime.date(2001, 9, 11)


class TestTime:

def test_serialize(self):
field = Time()
assert field.serialize(datetime.time(12, 5, 48)) == '12:05:48'

field = Time(format='%H%M%S')
assert field.serialize(datetime.time(12, 5, 48)) == '120548'

def test_deserialize(self):
field = Time()
assert field.deserialize('12:05:48') == datetime.time(12, 5, 48)

field = Time(format='%H%M%S')
assert field.deserialize('120548') == datetime.time(12, 5, 48)


class TestDomain:

def test_validate(self):
Expand Down