Skip to content

Commit

Permalink
Add support for year, yearmonth and duration field types (#152)
Browse files Browse the repository at this point in the history
* Add support for year, yearmonth and duration types

* Restore IntegerTypeTest and fix static analysis errors
  • Loading branch information
rflprr authored and roll committed Mar 25, 2017
1 parent 3b4f14d commit f344d2b
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 3 deletions.
2 changes: 1 addition & 1 deletion jsontableschema/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.0
0.9.1
12 changes: 12 additions & 0 deletions jsontableschema/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ class InvalidDateTimeType(InvalidCastError):
pass


class InvalidYearType(InvalidCastError):
pass


class InvalidYearMonthType(InvalidCastError):
pass


class InvalidDurationType(InvalidCastError):
pass


class InvalidTimeType(InvalidCastError):
pass

Expand Down
3 changes: 3 additions & 0 deletions jsontableschema/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@ def __validate_maxLength(self, value):
'date': types.DateType,
'time': types.TimeType,
'datetime': types.DateTimeType,
'year': types.YearType,
'yearmonth': types.YearMonthType,
'geopoint': types.GeoPointType,
'geojson': types.GeoJSONType,
'duration': types.DurationType,
'any': types.AnyType,
}
1 change: 1 addition & 0 deletions jsontableschema/infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,5 @@ def _get_available_types():
types.ObjectType,
types.GeoPointType,
types.GeoJSONType,
types.DurationType,
]
2 changes: 1 addition & 1 deletion jsontableschema/schemas/json-table-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"type": "string"
},
"type": {
"enum": [ "string", "number", "integer", "date", "time", "datetime", "boolean", "binary", "object", "geopoint", "geojson", "array", "any" ]
"enum": [ "string", "number", "integer", "date", "time", "datetime", "year", "yearmonth", "boolean", "binary", "object", "geopoint", "geojson", "array", "duration", "any" ]
},
"format": {
"type": "string"
Expand Down
3 changes: 3 additions & 0 deletions jsontableschema/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .boolean import BooleanType
from .date import DateType
from .datetime import DateTimeType
from .duration import DurationType
from .geojson import GeoJSONType
from .geopoint import GeoPointType
from .integer import IntegerType
Expand All @@ -18,3 +19,5 @@
from .object import ObjectType
from .string import StringType
from .time import TimeType
from .year import YearType
from .yearmonth import YearMonthType
41 changes: 41 additions & 0 deletions jsontableschema/types/duration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import isodate
from future.utils import raise_with_traceback

from . import base
from .. import exceptions
from .. import helpers


# Module API

class DurationType(base.JTSType):
# Public

name = 'duration'
null_values = helpers.NULL_VALUES
supported_constraints = [
'required',
'unique',
'enum',
'minimum',
'maximum',
]
# ---
python_type = isodate.Duration
formats = 'default'

def cast_default(self, value, fmt=None):

if isinstance(value, self.python_type):
return value

try:
return isodate.parse_duration(value)
except isodate.ISO8601Error as e:
raise_with_traceback(exceptions.InvalidDurationType(e))
46 changes: 46 additions & 0 deletions jsontableschema/types/year.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import decimal

from future.utils import raise_with_traceback

from . import base
from .. import exceptions
from .. import helpers


# Module API

class YearType(base.JTSType):
# Public

name = 'year'
null_values = helpers.NULL_VALUES
supported_constraints = [
'required',
'unique',
'enum',
'minimum',
'maximum',
]
# ---
python_type = int
formats = 'default'

def cast_default(self, value, fmt=None):

if isinstance(value, self.python_type):
return value

if len(value) > 4:
raise exceptions.InvalidYearType(
'{0} is not a valid year value'.format(value))

try:
return self.python_type(value)
except (ValueError, TypeError, decimal.InvalidOperation) as e:
raise_with_traceback(exceptions.InvalidYearType(e))
47 changes: 47 additions & 0 deletions jsontableschema/types/yearmonth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import decimal

from future.utils import raise_with_traceback

from . import base
from .. import exceptions
from .. import helpers


# Module API

class YearMonthType(base.JTSType):
# Public

name = 'yearmonth'
null_values = helpers.NULL_VALUES
supported_constraints = [
'required',
'unique',
'pattern',
'enum',
'minimum',
'maximum',
]
# ---
python_type = int
formats = 'default'

def cast_default(self, value, fmt=None):

if isinstance(value, self.python_type):
return value

try:
cast_value = self.python_type(value)
if not (1 <= cast_value <= 12):
raise exceptions.InvalidYearMonthType(
'{0} is not a valid yearmonth value'.format(value))
return cast_value
except (ValueError, TypeError, decimal.InvalidOperation) as e:
raise_with_traceback(exceptions.InvalidYearMonthType(e))
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def read(*paths):
'future>=0.15,<1.0a',
'unicodecsv>=0.14,<1.0a',
'tabulator>=0.7,<1.0a',
'isodate>=0.5.4,<1.0a',
]
TESTS_REQUIRE = [
'pylama',
Expand Down
93 changes: 92 additions & 1 deletion tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
from __future__ import print_function
from __future__ import unicode_literals

import isodate
import pytest
from datetime import datetime, date, time
from datetime import datetime, date, time, timedelta
from decimal import Decimal
from jsontableschema import types, exceptions
from . import base
Expand Down Expand Up @@ -603,6 +604,96 @@ def test_datetime_type_with_already_cast_value(self):
self.assertEqual(_type.cast(value), value)


class TestYear(base.BaseTestCase):
def setUp(self):
super(TestYear, self).setUp()
self.field = {
'name': 'Name',
'type': 'year',
'format': 'default',
'constraints': {
'required': True
}
}

def test_year_type_simple(self):
value = '2008'
_type = types.YearType(self.field)

self.assertEquals(_type.cast(value), int(value))

def test_year_type_simple_raises(self):
value = '202020'
_type = types.YearType(self.field)

self.assertRaises(exceptions.InvalidYearType, _type.cast, value)

def test_year_type_with_already_cast_value(self):
value = 2008
_type = types.YearType(self.field)
self.assertEqual(_type.cast(value), value)


class TestYearMonth(base.BaseTestCase):
def setUp(self):
super(TestYearMonth, self).setUp()
self.field = {
'name': 'Name',
'type': 'yearmonth',
'format': 'default',
'constraints': {
'required': True
}
}

def test_yearmonth_type_simple(self):
value = '4'
_type = types.YearMonthType(self.field)

self.assertEquals(_type.cast(value), int(value))

def test_yearmonth_type_simple_raises(self):
value = '13'
_type = types.YearMonthType(self.field)

self.assertRaises(exceptions.InvalidYearMonthType, _type.cast, value)

def test_yearmonth_type_with_already_cast_value(self):
for value in range(1, 13):
_type = types.YearMonthType(self.field)
self.assertEqual(_type.cast(value), value)


class TestDuration(base.BaseTestCase):
def setUp(self):
super(TestDuration, self).setUp()
self.field = {
'name': 'Name',
'type': 'duration',
'format': 'default',
'constraints': {
'required': True
}
}

def test_duration_type_simple(self):
value = 'P1Y'
_type = types.DurationType(self.field)

self.assertEquals(_type.cast(value), isodate.Duration(years=1))

def test_duration_type_simple_raises(self):
value = '1Y'
_type = types.DurationType(self.field)

self.assertRaises(exceptions.InvalidDurationType, _type.cast, value)

def test_yearmonth_type_with_already_cast_value(self):
for value in [isodate.Duration(years=1)]:
_type = types.DurationType(self.field)
self.assertEqual(_type.cast(value), value)


class TestGeoPoint(base.BaseTestCase):
def setUp(self):
super(TestGeoPoint, self).setUp()
Expand Down

0 comments on commit f344d2b

Please sign in to comment.