adding datetime, date, time and timedelta validation

This commit is contained in:
Samuel Colvin
2017-05-05 19:57:15 +01:00
parent 90bc681137
commit c859ec8543
5 changed files with 110 additions and 45 deletions
+2 -1
View File
@@ -9,7 +9,8 @@ Data validation and settings management using python 3.6 type hinting.
TODO:
* datetime types, enums
* allow and disallow extra
* enums
* other types: regex, email field
* exotic typing: Union, List, Dict
* more info: alt names and descriptions
+2 -3
View File
@@ -12,7 +12,7 @@ Stolen from https://raw.githubusercontent.com/django/django/master/django/utils/
Changed to:
* use standard python datetime types not django.utils.timezone
* raise ValueError when regex doesn't match rather than returning None
* support parsing unix timestamps
* support parsing unix timestamps for dates and datetimes
"""
import re
from datetime import date, datetime, time, timedelta, timezone
@@ -42,8 +42,7 @@ standard_duration_re = re.compile(
r'$'
)
# Support the sections of ISO 8601 date representation that are accepted by
# timedelta
# Support the sections of ISO 8601 date representation that are accepted by timedelta
iso8601_duration_re = re.compile(
r'^(?P<sign>[-+]?)'
r'P'
+10 -2
View File
@@ -1,6 +1,9 @@
from datetime import date, datetime, time, timedelta
from pathlib import Path
from typing import Optional
from .datetime_parse import parse_date, parse_datetime, parse_duration, parse_time
NoneType = type(None)
@@ -73,7 +76,12 @@ VALIDATORS_LOOKUP = {
Optional[bytes]: [bytes_validator, anystr_length_validator],
bytes: [not_none_validator, bytes_validator, anystr_length_validator],
dict: [not_none_validator, dict_validator]
dict: [not_none_validator, dict_validator],
# TODO list, List, Dict, Union, datetime, date, time, custom types
date: [parse_date],
time: [parse_time],
datetime: [parse_datetime],
timedelta: [parse_duration],
# TODO list, List, Dict, Union
}
+55 -38
View File
@@ -17,55 +17,72 @@ def create_tz(minutes):
return timezone(timedelta(minutes=minutes))
@pytest.mark.parametrize('func,value,result', [
@pytest.mark.parametrize('value,result', [
# Valid inputs
(parse_date, '1494012444.883309', date(2017, 5, 5)),
(parse_date, 1494012444.883309, date(2017, 5, 5)),
(parse_date, '1494012444', date(2017, 5, 5)),
(parse_date, 1494012444, date(2017, 5, 5)),
(parse_date, '2012-04-23', date(2012, 4, 23)),
(parse_date, '2012-4-9', date(2012, 4, 9)),
('1494012444.883309', date(2017, 5, 5)),
(1494012444.883309, date(2017, 5, 5)),
('1494012444', date(2017, 5, 5)),
(1494012444, date(2017, 5, 5)),
('2012-04-23', date(2012, 4, 23)),
('2012-4-9', date(2012, 4, 9)),
# Invalid inputs
(parse_date, 'x20120423', ValueError),
(parse_date, '2012-04-56', ValueError),
('x20120423', ValueError),
('2012-04-56', ValueError),
])
def test_date_parsing(value, result):
if result == ValueError:
with pytest.raises(ValueError):
parse_date(value)
else:
assert parse_date(value) == result
@pytest.mark.parametrize('value,result', [
# Valid inputs
(parse_time, '09:15:00', time(9, 15)),
(parse_time, '10:10', time(10, 10)),
(parse_time, '10:20:30.400', time(10, 20, 30, 400000)),
(parse_time, '4:8:16', time(4, 8, 16)),
('09:15:00', time(9, 15)),
('10:10', time(10, 10)),
('10:20:30.400', time(10, 20, 30, 400000)),
('4:8:16', time(4, 8, 16)),
# Invalid inputs
(parse_time, '091500', ValueError),
(parse_time, '09:15:90', ValueError),
('091500', ValueError),
('09:15:90', ValueError),
])
def test_time_parsing(value, result):
if result == ValueError:
with pytest.raises(ValueError):
parse_time(value)
else:
assert parse_time(value) == result
@pytest.mark.parametrize('value,result', [
# Valid inputs
# values in seconds
(parse_datetime, '1494012444.883309', datetime(2017, 5, 5, 19, 27, 24, 883309, tzinfo=timezone.utc)),
(parse_datetime, 1494012444.883309, datetime(2017, 5, 5, 19, 27, 24, 883309, tzinfo=timezone.utc)),
(parse_datetime, '1494012444', datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
(parse_datetime, 1494012444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
('1494012444.883309', datetime(2017, 5, 5, 19, 27, 24, 883309, tzinfo=timezone.utc)),
(1494012444.883309, datetime(2017, 5, 5, 19, 27, 24, 883309, tzinfo=timezone.utc)),
('1494012444', datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
(1494012444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
# values in ms
(parse_datetime, '1494012444000.883309', datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)),
(parse_datetime, 1494012444000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
('1494012444000.883309', datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)),
(1494012444000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)),
(parse_datetime, '2012-04-23T09:15:00', datetime(2012, 4, 23, 9, 15)),
(parse_datetime, '2012-4-9 4:8:16', datetime(2012, 4, 9, 4, 8, 16)),
(parse_datetime, '2012-04-23T09:15:00Z', datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)),
(parse_datetime, '2012-4-9 4:8:16-0320', datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))),
(parse_datetime, '2012-04-23T10:20:30.400+02:30', datetime(2012, 4, 23, 10, 20, 30, 400000, create_tz(150))),
(parse_datetime, '2012-04-23T10:20:30.400+02', datetime(2012, 4, 23, 10, 20, 30, 400000, create_tz(120))),
(parse_datetime, '2012-04-23T10:20:30.400-02', datetime(2012, 4, 23, 10, 20, 30, 400000, create_tz(-120))),
('2012-04-23T09:15:00', datetime(2012, 4, 23, 9, 15)),
('2012-4-9 4:8:16', datetime(2012, 4, 9, 4, 8, 16)),
('2012-04-23T09:15:00Z', datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)),
('2012-4-9 4:8:16-0320', datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))),
('2012-04-23T10:20:30.400+02:30', datetime(2012, 4, 23, 10, 20, 30, 400000, create_tz(150))),
('2012-04-23T10:20:30.400+02', datetime(2012, 4, 23, 10, 20, 30, 400000, create_tz(120))),
('2012-04-23T10:20:30.400-02', datetime(2012, 4, 23, 10, 20, 30, 400000, create_tz(-120))),
# Invalid inputs
(parse_datetime, 'x20120423091500', ValueError),
(parse_datetime, '2012-04-56T09:15:90', ValueError),
('x20120423091500', ValueError),
('2012-04-56T09:15:90', ValueError),
])
def test_parsing(func, value, result):
if isinstance(result, type) and issubclass(result, BaseException):
with pytest.raises(result):
func(value)
def test_datetime_parsing(value, result):
if result == ValueError:
with pytest.raises(ValueError):
parse_datetime(value)
else:
print(repr(func(value)))
assert func(value) == result
assert parse_datetime(value) == result
@pytest.mark.parametrize('delta', [
@@ -121,8 +138,8 @@ def test_parse_python_format(delta):
('PT0.000005S', timedelta(microseconds=5)),
])
def test_parse_durations(value, result):
if isinstance(result, type) and issubclass(result, BaseException):
with pytest.raises(result):
if result == ValueError:
with pytest.raises(ValueError):
parse_duration(value)
else:
assert parse_duration(value) == result
+41 -1
View File
@@ -1,4 +1,6 @@
import os
from collections import OrderedDict
from datetime import date, datetime, time, timedelta
import pytest
@@ -123,10 +125,48 @@ class CheckModel(BaseModel):
('float_check', '123', ValidationError),
('float_check', b'123', ValidationError),
])
def test_bool_validation(field, value, result):
def test_default_validators(field, value, result):
kwargs = {field: value}
if result == ValidationError:
with pytest.raises(ValidationError):
CheckModel(**kwargs)
else:
assert CheckModel(**kwargs).values[field] == result
class DatetimeModel(BaseModel):
dt: datetime = ...
date_: date = ...
time_: time = ...
duration: timedelta = ...
def test_datetime_successful():
m = DatetimeModel(
dt='2017-10-5T19:47:07',
date_=1494012000,
time_='10:20:30.400',
duration='15:30.0001',
)
assert m.dt == datetime(2017, 10, 5, 19, 47, 7)
assert m.date_ == date(2017, 5, 5)
assert m.time_ == time(10, 20, 30, 400000)
assert m.duration == timedelta(minutes=15, seconds=30, microseconds=100)
def test_datetime_errors():
with pytest.raises(ValueError) as exc_info:
DatetimeModel(
dt='2017-13-5T19:47:07',
date_='XX1494012000',
time_='25:20:30.400',
duration='15:30.0001 broken',
)
assert exc_info.value.args[0].startswith('4 errors validating input:')
assert exc_info.value.errors == OrderedDict(
[
('dt', {'type': 'ValueError', 'msg': 'month must be in 1..12', 'validator': 'parse_datetime'}),
('date_', {'type': 'ValueError', 'msg': 'Invalid date format', 'validator': 'parse_date'}),
('time_', {'type': 'ValueError', 'msg': 'hour must be in 0..23', 'validator': 'parse_time'}),
('duration', {'type': 'ValueError', 'msg': 'Invalid duration format', 'validator': 'parse_duration'})
])