Skip to content

Commit 02f6a8a

Browse files
committed
Add builtin 'time' format validation
This is similar to the `date-time` case, and is tested in a similar manner.
1 parent 4c39842 commit 02f6a8a

File tree

6 files changed

+107
-17
lines changed

6 files changed

+107
-17
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ Unreleased
1111
.. vendor-insert-here
1212
1313
- Update vendored schemas (2024-02-05)
14-
- Include a built-in, efficient implementation of `date-time` format validation
15-
(RFC 3339). This makes the `date-time` format always available for
16-
validation. (:issue:`378`)
14+
- Include built-in, efficient implementations of `date-time` format validation
15+
(RFC 3339) and `time` format validation (ISO 8601). This makes the `date-time`
16+
and `time` formats always available for validation. (:issue:`378`)
1717
- Support the use of `orjson` for faster JSON parsing when it is installed.
1818
This makes it an optional parser which is preferred over the default
1919
`json` module when it is available.

src/check_jsonschema/formats/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import jsonschema.validators
1010
import regress
1111

12-
from .implementations import validate_rfc3339
12+
from .implementations import validate_rfc3339, validate_time
1313

1414
# all known format strings except for a selection from draft3 which have either
1515
# been renamed or removed:
@@ -104,6 +104,7 @@ def make_format_checker(
104104
regex_impl = RegexImplementation(opts.regex_variant)
105105
checker.checks("regex")(regex_impl.check_format)
106106
checker.checks("date-time")(validate_rfc3339)
107+
checker.checks("time")(validate_time)
107108

108109
# remove the disabled checks, which may include the regex check
109110
for checkname in opts.disabled_formats:
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .iso8601_time import validate as validate_time
12
from .rfc3339 import validate as validate_rfc3339
23

3-
__all__ = ("validate_rfc3339",)
4+
__all__ = ("validate_rfc3339", "validate_time")
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import re
2+
3+
TIME_REGEX = re.compile(
4+
r"""
5+
^
6+
(?:[01]\d|2[0123])
7+
:
8+
(?:[0-5]\d)
9+
:
10+
(?:[0-5]\d)
11+
# (optional) fractional seconds
12+
(?:(\.|,)\d+)?
13+
# UTC or offset
14+
(?:
15+
Z
16+
| z
17+
| [+-](?:[01]\d|2[0123]):[0-5]\d
18+
)
19+
$
20+
""",
21+
re.VERBOSE | re.ASCII,
22+
)
23+
24+
25+
def validate(time_str: object) -> bool:
26+
if not isinstance(time_str, str):
27+
return False
28+
return bool(TIME_REGEX.match(time_str))
29+
30+
31+
if __name__ == "__main__":
32+
import timeit
33+
34+
N = 100_000
35+
tests = (
36+
("basic", "23:59:59Z"),
37+
("long_fracsec", "23:59:59.8446519776713Z"),
38+
)
39+
40+
print("benchmarking")
41+
for name, val in tests:
42+
all_times = timeit.repeat(
43+
f"validate({val!r})", globals=globals(), repeat=3, number=N
44+
)
45+
print(f"{name} (valid={validate(val)}): {int(min(all_times) / N * 10**9)}ns")

src/check_jsonschema/formats/implementations/rfc3339.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import re
2-
import typing as t
32

43
# this regex is based on the one from the rfc3339-validator package
54
# credit to the original author
@@ -56,7 +55,7 @@
5655
)
5756

5857

59-
def validate(date_str: t.Any) -> bool:
58+
def validate(date_str: object) -> bool:
6059
"""Validate a string as a RFC3339 date-time."""
6160
if not isinstance(date_str, str):
6261
return False
@@ -80,18 +79,15 @@ def validate(date_str: t.Any) -> bool:
8079
import timeit
8180

8281
N = 100_000
83-
long_fracsec = "2018-12-31T23:59:59.8446519776713Z"
84-
basic = "2018-12-31T23:59:59Z"
85-
in_february = "2018-02-12T23:59:59Z"
86-
in_february_invalid = "2018-02-29T23:59:59Z"
82+
tests = (
83+
("long_fracsec", "2018-12-31T23:59:59.8446519776713Z"),
84+
("basic", "2018-12-31T23:59:59Z"),
85+
("in_february", "2018-02-12T23:59:59Z"),
86+
("in_february_invalid", "2018-02-29T23:59:59Z"),
87+
)
8788

8889
print("benchmarking")
89-
for name, val in (
90-
("long_fracsec", long_fracsec),
91-
("basic", basic),
92-
("february", in_february),
93-
("february_invalid", in_february_invalid),
94-
):
90+
for name, val in tests:
9591
all_times = timeit.repeat(
9692
f"validate({val!r})", globals=globals(), repeat=3, number=N
9793
)

tests/unit/formats/test_time.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import random
2+
3+
import pytest
4+
5+
from check_jsonschema.formats.implementations.iso8601_time import validate
6+
7+
8+
@pytest.mark.parametrize(
9+
"timestr",
10+
(
11+
"12:34:56Z",
12+
"23:59:59z",
13+
"23:59:59+00:00",
14+
"01:59:59-00:00",
15+
),
16+
)
17+
def test_simple_positive_cases(timestr):
18+
assert validate(timestr)
19+
20+
21+
@pytest.mark.parametrize(
22+
"timestr",
23+
(
24+
"12:34:56",
25+
"23:59:60Z",
26+
"23:59:59+24:00",
27+
"01:59:59-00:60",
28+
"01:01:00:00:60",
29+
),
30+
)
31+
def test_simple_negative_cases(timestr):
32+
assert not validate(timestr)
33+
34+
35+
@pytest.mark.parametrize("precision", list(range(20)))
36+
@pytest.mark.parametrize(
37+
"offsetstr",
38+
(
39+
"Z",
40+
"+00:00",
41+
"-00:00",
42+
"+23:59",
43+
),
44+
)
45+
def test_allows_fracsec(precision, offsetstr):
46+
fracsec = random.randint(0, 10**precision)
47+
assert validate(f"23:59:59.{fracsec}{offsetstr}")

0 commit comments

Comments
 (0)