diff --git a/README.md b/README.md index 3ac18d4..72db87b 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,21 @@ _usage:_ ```$.my.path.`datetime({strptime_str}, {return_slice})` ``` *_White space after commas is required!_* +### Parse Epoch Timestamp + +Attempts to parse the epoch timestamp from a value found at a given jsonpath. Then returns a part or the whole of the parsed datetime as specified by a python array slice. Will operate invidiually on array items in the case the path resolves to an array. The input units must be indicated as one of: + + - `seconds` + - `millis` + - `micros` + + +The array slice notation used is the same as in `datetime`. + +_usage:_ ```$.my.path.`epoch({units}, {return_slice})` ``` + +*_White space after commas is required!_* + ### Hash Returns a 128bit, hex formatted MD5 hash of the value of a resolved jsonpath added to a provided `salt`. This is useful for generating a UUID compatable value from a piece of source information along with some unique salt. This allows for a non-UUID foreign key to be reliably converted to the same UUID compatable value every time. Will operate invidiually on array items in the case the path resolves to an array. diff --git a/eha_jsonpath/__init__.py b/eha_jsonpath/__init__.py index db5a546..88a2a6e 100644 --- a/eha_jsonpath/__init__.py +++ b/eha_jsonpath/__init__.py @@ -17,6 +17,8 @@ def p_jsonpath_named_operator(self, p): p[0] = fn.Match(p[1]) elif p[1].startswith("notmatch("): p[0] = fn.NotMatch(p[1]) + elif p[1].startswith("epoch("): + p[0] = fn.ParseEpochDatetime(p[1]) elif p[1].startswith("datetime("): p[0] = fn.ParseDatetime(p[1]) elif p[1].startswith("hash("): diff --git a/eha_jsonpath/ext_functions.py b/eha_jsonpath/ext_functions.py index b5afd64..1073598 100644 --- a/eha_jsonpath/ext_functions.py +++ b/eha_jsonpath/ext_functions.py @@ -197,6 +197,35 @@ def _do(self, obj): return value +class ParseEpochDatetime(BaseFn): + ''' + usage: `epoch({unit}, {return_slice})` + ! White space after commas is required! + result is : iso_datetime[return_slice] + ''' + METHOD_SIG = re.compile(r'epoch\((.+),\s+(.+)\)') + VALID_FORMATS = { + 'second': 1, + 'millis': 1_000, + 'micros': 1_000_000 + } + + def __init__(self, method=None): + args = self.get_args(method) + self.time_format = args[0] + self.slice_arg = args[1] + self.method = method + + def _do(self, obj): + if self.time_format not in self.VALID_FORMATS: + raise DefintionInvalid( + f'{self.time_format} is invalid: excepted {self.VALID_FORMATS.keys()}') + deno = self.VALID_FORMATS[self.time_format] + value = datetime.fromtimestamp(float(obj) / deno) + return ParseDatetime.args_to_slice( + self.slice_arg, value.isoformat()) + + class Hash(BaseFn): ''' usage: `hash({salt})` diff --git a/test/test.py b/test/test.py index 6a573c6..44f2890 100644 --- a/test/test.py +++ b/test/test.py @@ -1,4 +1,5 @@ import pytest +import uuid from eha_jsonpath import parse from eha_jsonpath.ext_functions import BaseFn @@ -9,6 +10,9 @@ 'comma': '1,2,3,4', 'float': '1.04', 'bad_float': '1.04s', + 'epoch1': '0', + 'epoch2': 1_000_000_000, + 'epoch3': 1_000_000_000_000_000, 'int': '1.09', 'str': 1.0, 'str2': 'a', @@ -109,6 +113,27 @@ ('$.boolean2.`match(1, 0)`', [True], False), + ('$.epoch1.`epoch(second, 0:4)`', + ['1970'], + False), + ('$.epoch1.`epoch(millis, 0:4)`', + ['1970'], + False), + ('$.epoch1.`epoch(micros, 0:4)`', + ['1970'], + False), + ('$.epoch1.`epoch(other, 0:4)`', + ['1970'], + True), + ('$.epoch2.`epoch(second, 0:4)`', + ['2001'], + False), + ('$.epoch2.`epoch(millis, 0:4)`', + ['1970'], + False), + ('$.epoch3.`epoch(micros, 0:4)`', + ['2001'], + False), ('$.dt1.`datetime(%Y-%m-%d, 0:4)`', ['2019'], False), @@ -200,3 +225,19 @@ def test_hashs_equality(cmd1, cmd2): ]) def test_hashs_inequality(cmd1, cmd2): parse(cmd1).find(src)[0] != parse(cmd2).find(src)[0] + + +@pytest.mark.parametrize("cmd1", [ + ('$.hashable1.`hash(a)`'), + ('$.hashable2.`hash(b)`'), + ('$.hashable3.`hash(a)`'), + ('$.hashable4.`hash(b)`') +]) +def test_hash_is_uuid(cmd1): + try: + id_ = parse(cmd1).find(src)[0].value + uuid.UUID(id_, version=4) + except (ValueError, AttributeError, TypeError) as err: + assert(False), (id_, err) + else: + assert(True)