Skip to content

Commit

Permalink
Add support for parsing timestamps with python-dateutil library. This…
Browse files Browse the repository at this point in the history
… adds a new runtime dependency but it will make the parsing alot easier then to do it manually with builtin datetime.
  • Loading branch information
Grokzen committed Apr 4, 2015
1 parent 7576190 commit c5a4782
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 6 deletions.
2 changes: 2 additions & 0 deletions ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Release Notes
=====

- Rework cli string that docopt uses. Removed redundant flags that docopt provides [--version & --help]
- Add support for timestamp validation
- Add new runtime dependency 'python-dateutil' that is used to validate timestamps

1.0.1
=====
Expand Down
7 changes: 6 additions & 1 deletion docs/Validation Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The followings are available:
- text (str or number)
- date [NYI]
- time [NYI]
- timestamp [NYI]
- timestamp
- sequence or seq
- mapping or map
- none
Expand Down Expand Up @@ -100,6 +100,11 @@ Name of schema.
Description is not used for validation.


## timestamp

Parse a string to determine if it is a valid timestamp. Parsing is done with `python-dateutil` lib and see all valid formats at `https://dateutil.readthedocs.org/en/latest/examples.html#parse-examples`.


## allowempty

NOTE: Experimental feature!
Expand Down
16 changes: 16 additions & 0 deletions pykwalify/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

# 3rd party imports
import yaml
from dateutil.parser import parse

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -397,6 +398,21 @@ def _validate_scalar(self, value, rule, path, errors=[], done=None):
"scalar",
)

# Validate timestamp
if rule._type == "timestamp":
v = value.strip()

# parse("") will give a valid date but it should not be
# considered a valid timestamp
if v == "":
errors.append("timestamp.empty : {} : {}".format(value, path))
else:
try:
parse(value)
# If it can be parsed then it is valid
except Exception:
errors.append("timestamp.invalid : {} : {}".format(value, path))

def _validate_range(self, max_, min_, max_ex, min_ex, errors, value, path, prefix):
"""
Validate that value is within range values.
Expand Down
17 changes: 13 additions & 4 deletions pykwalify/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
""" pyKwalify - types.py """

# python std lib
import datetime
from datetime import datetime

DEFAULT_TYPE = "str"

Expand All @@ -15,8 +15,8 @@
"bool": bool,
"map": dict,
"seq": list,
"timestamp": datetime.datetime,
"date": datetime.datetime,
"timestamp": datetime,
"date": datetime,
"symbol": str,
"scalar": None,
"text": None,
Expand Down Expand Up @@ -102,6 +102,14 @@ def is_mapping_alias(alias):
return alias in mapping_aliases


def is_timestamp(obj):
"""
Yaml either have automatically converted it to a datetime object
or it is a string that will be validated later.
"""
return isinstance(obj, datetime) or isinstance(obj, str)


tt = {
"str": is_string,
"int": is_int,
Expand All @@ -111,5 +119,6 @@ def is_mapping_alias(alias):
"text": is_text,
"any": is_any,
"enum": is_enum,
"none": is_none
"none": is_none,
"timestamp": is_timestamp,
}
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
docopt==0.6.2
PyYAML==3.11
python-dateutil==2.4.2
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
install_requires=[
'docopt==0.6.2',
'PyYAML==3.11',
'python-dateutil==2.4.2',
],
classifiers=(
# 'Development Status :: 1 - Planning',
Expand Down
13 changes: 13 additions & 0 deletions tests/files/fail/15f.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
data:
d1: ""
d2: "1427650980"
schema:
type: map
mapping:
d1:
type: timestamp
d2:
type: timestamp
errors:
- 'timestamp.empty : : /d1'
- 'timestamp.invalid : 1427650980 : /d2'
16 changes: 16 additions & 0 deletions tests/files/success/26s.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
data:
d1: "2015-03-29T18:45:00+00:00"
d2: "2015-03-29T18:45:00"
d3: "2015-03-29T11:45:00 -0700"
d4: "2015-03-29"
schema:
type: map
mapping:
d1:
type: timestamp
d2:
type: timestamp
d3:
type: timestamp
d4:
type: timestamp
6 changes: 5 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def test_multi_file_support(self):

compare(sorted(c.validation_errors), sorted(failing_test[3]), prefix="Wrong validation errors when parsing files : {} : {}".format(failing_test[0], failing_test[1]))

def testCore(self):
def test_core_files(self):
# These tests should pass with no exception raised
pass_tests = [
# Test sequence with only string values
Expand Down Expand Up @@ -301,6 +301,8 @@ def testCore(self):
"24s.yaml",
# Test that there is no need to specify 'type: seq' or 'type: map'
"25s.yaml",
# Test that the different types of timestamps can be validated
"26s.yaml",
]

_fail_tests = [
Expand Down Expand Up @@ -332,6 +334,8 @@ def testCore(self):
("13f.yaml", SchemaError),
# Test that range validates on 'seq' raise correct error
("14f.yaml", SchemaError),
# Test timestamps that should throw errors
("15f.yaml", SchemaError),
]

for passing_test_file in pass_tests:
Expand Down

0 comments on commit c5a4782

Please sign in to comment.