jsonschema-extras 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jsonschema_extras/VERSION +1 -0
- jsonschema_extras/__init__.py +96 -0
- jsonschema_extras/_common.py +31 -0
- jsonschema_extras/_util/__init__.py +21 -0
- jsonschema_extras/_util/tests/__init__.py +0 -0
- jsonschema_extras/_util/tests/test_uri.py +210 -0
- jsonschema_extras/_util/uri.py +125 -0
- jsonschema_extras/formats/__init__.py +30 -0
- jsonschema_extras/formats/_common.py +74 -0
- jsonschema_extras/formats/numbers_range.py +57 -0
- jsonschema_extras/formats/slice_string.py +69 -0
- jsonschema_extras/formats/tests/__init__.py +0 -0
- jsonschema_extras/formats/tests/test_numbers_range.py +22 -0
- jsonschema_extras/formats/tests/test_slice_string.py +39 -0
- jsonschema_extras/registries/__init__.py +25 -0
- jsonschema_extras/registries/_common.py +209 -0
- jsonschema_extras/registries/filesystem.py +330 -0
- jsonschema_extras/registries/retrieval/__init__.py +184 -0
- jsonschema_extras/registries/retrieval/_common.py +14 -0
- jsonschema_extras/registries/retrieval/json.py +18 -0
- jsonschema_extras/registries/tests/__init__.py +0 -0
- jsonschema_extras/registries/tests/test_filesystem.py +131 -0
- jsonschema_extras/schemas/common/range.json +15 -0
- jsonschema_extras/schemas/common/range_integer.json +12 -0
- jsonschema_extras/schemas/common/slice_object.json +29 -0
- jsonschema_extras/schemas/common/slice_string.json +21 -0
- jsonschema_extras/typing.py +4 -0
- jsonschema_extras-0.2.0.dist-info/METADATA +52 -0
- jsonschema_extras-0.2.0.dist-info/RECORD +32 -0
- jsonschema_extras-0.2.0.dist-info/WHEEL +5 -0
- jsonschema_extras-0.2.0.dist-info/licenses/LICENSE.txt +21 -0
- jsonschema_extras-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.0
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Utilities for working with the JSON Schema Python library
|
|
2
|
+
`jsonschema <https://pypi.org/project/jsonschema/>`__.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
|
|
6
|
+
- :doc:`Schemas bundled with the library </schemas>`
|
|
7
|
+
|
|
8
|
+
- :doc:`Formats bundled with the library </formats>`
|
|
9
|
+
|
|
10
|
+
- :doc:`Utilities for accessing schemas on a filesystem </filesystem>`
|
|
11
|
+
|
|
12
|
+
- :doc:`Other generally useful utilities </misc>`
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
import importlib.resources
|
|
18
|
+
from importlib.resources.abc import Traversable
|
|
19
|
+
from typing import Any, Final
|
|
20
|
+
|
|
21
|
+
from referencing.typing import Retrieve
|
|
22
|
+
|
|
23
|
+
from .registries import build_schemas_from_filesystem_retriever
|
|
24
|
+
from .registries.retrieval import CacheFn, CacheSpecDefault
|
|
25
|
+
from .registries.retrieval.json import LOADS_FN_JSON_DEFAULT
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = (
|
|
29
|
+
'bundled_schemas_files',
|
|
30
|
+
'BUNDLED_SCHEMAS_URI_BASE',
|
|
31
|
+
'bundled_schemas_retriever',
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def bundled_schemas_files() -> Traversable:
|
|
36
|
+
"""Returns a :class:`~importlib.resources.abc.Traversable` object
|
|
37
|
+
containing resources for bundled schemas.
|
|
38
|
+
|
|
39
|
+
Built upon :func:`importlib.resources.files`.
|
|
40
|
+
"""
|
|
41
|
+
return (importlib.resources.files('jsonschema_extras') / 'schemas')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
#: Base URI for schemas bundled with this library.
|
|
45
|
+
#:
|
|
46
|
+
#: See Also:
|
|
47
|
+
#: :func:`bundled_schemas_retriever`
|
|
48
|
+
BUNDLED_SCHEMAS_URI_BASE: Final[str] = 'file:/jsonschema_extras/schemas'
|
|
49
|
+
|
|
50
|
+
_BUNDLED_SCHEMAS_ENCODING: Final = 'utf-8'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@contextmanager
|
|
54
|
+
def bundled_schemas_retriever(
|
|
55
|
+
*, open_buffering: int = -1, cache: CacheFn[Any] | CacheSpecDefault | None = None,
|
|
56
|
+
) -> Iterator[Retrieve]:
|
|
57
|
+
"""**Context manager** producing a retrieval callable for this library's
|
|
58
|
+
bundled schemas.
|
|
59
|
+
|
|
60
|
+
Warning:
|
|
61
|
+
The produced retriever is only valid within duration of an explicit
|
|
62
|
+
lifecycle, hence the context manager.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
cache (CacheFn | CacheSpecDefault, optional):
|
|
66
|
+
Caching decorator for :class:`~referencing.typing.Retrieve`,
|
|
67
|
+
or ``'default'`` to use the default caching implementation
|
|
68
|
+
(see description of :mod:`jsonschema_extras.registries.retrieval`
|
|
69
|
+
for details). Defaults to ``None``, meaning no caching.
|
|
70
|
+
open_buffering (int, optional):
|
|
71
|
+
Optional integer used to set the buffering policy.
|
|
72
|
+
See the Python built-in function :func:`open`.
|
|
73
|
+
|
|
74
|
+
Yields:
|
|
75
|
+
A retrieval callable for this library's bundled schemas.
|
|
76
|
+
The retriever is only valid until the context is exited.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValueError: On invalid `uri_base`.
|
|
80
|
+
|
|
81
|
+
Note:
|
|
82
|
+
The implementation uses :mod:`importlib.resources` which conceptually
|
|
83
|
+
cannot provide a persistent path to a directory which could be accessed
|
|
84
|
+
by common code at any time. The lifecycle has to be explicit
|
|
85
|
+
due to using package resources.
|
|
86
|
+
"""
|
|
87
|
+
with importlib.resources.as_file(bundled_schemas_files()) as bundled_schemas_path:
|
|
88
|
+
yield build_schemas_from_filesystem_retriever(
|
|
89
|
+
BUNDLED_SCHEMAS_URI_BASE,
|
|
90
|
+
bundled_schemas_path,
|
|
91
|
+
open_kwargs=dict(
|
|
92
|
+
buffering=open_buffering, encoding=_BUNDLED_SCHEMAS_ENCODING,
|
|
93
|
+
),
|
|
94
|
+
cache=cache,
|
|
95
|
+
loads=LOADS_FN_JSON_DEFAULT,
|
|
96
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from collections.abc import Collection, Mapping
|
|
2
|
+
from typing import Any, TypeAlias
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = ()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
type Kwargs = Mapping[str, Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# NOTE: returns kwargs without changes
|
|
12
|
+
def validate_kwargs(
|
|
13
|
+
kwargs: Kwargs, *, allowed: Collection[str] = (), required: Collection[str] = (),
|
|
14
|
+
) -> Kwargs:
|
|
15
|
+
# NOTE: using lists instead of sets to preserve order
|
|
16
|
+
allowed = list(allowed)
|
|
17
|
+
allowed += [s for s in required if s not in allowed]
|
|
18
|
+
unexpected = [s for s in kwargs if s not in allowed]
|
|
19
|
+
if len(unexpected) > 0:
|
|
20
|
+
raise TypeError(
|
|
21
|
+
'got unexpected keyword argument(s): {0}'.format(', '.join(unexpected))
|
|
22
|
+
)
|
|
23
|
+
missing = [s for s in required if s not in kwargs]
|
|
24
|
+
if len(missing) > 0:
|
|
25
|
+
raise TypeError(
|
|
26
|
+
'missing required keyword argument(s): {0}'.format(', '.join(missing))
|
|
27
|
+
)
|
|
28
|
+
return kwargs
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
EncodingId: TypeAlias = str
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from collections.abc import Collection, Hashable, Mapping
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
_K = TypeVar('_K', bound=Hashable)
|
|
6
|
+
_V = TypeVar('_V')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
COLLECTION_PRIMITIVE_TYPES_DEFAULT: Collection[type] = (
|
|
10
|
+
str, bytes, bytearray, memoryview,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
SEQUENCE_PRIMITIVE_TYPES_DEFAULT: Collection[type] = (
|
|
14
|
+
*COLLECTION_PRIMITIVE_TYPES_DEFAULT,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def coerce_to_dict(mapping: Mapping[_K, _V]) -> dict[_K, _V]:
|
|
19
|
+
if isinstance(mapping, dict):
|
|
20
|
+
return mapping
|
|
21
|
+
return dict(mapping.items())
|
|
File without changes
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from pathlib import PurePosixPath
|
|
2
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from jsonschema_extras._util.uri import (
|
|
7
|
+
RPURIBValidateURIValueErrorCode,
|
|
8
|
+
RPURIBValidateURIValueError,
|
|
9
|
+
is_uri_absolute,
|
|
10
|
+
rpurib_validate_uri_base_split,
|
|
11
|
+
rpurib_validate_uri_split,
|
|
12
|
+
RPURIBNotRelativeValueError,
|
|
13
|
+
relative_path_from_uri_by_base_splits_validated,
|
|
14
|
+
translate_uri_base_splits_validated,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestIsUriAbsolute:
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize(
|
|
21
|
+
('uri', 'is_absolute'),
|
|
22
|
+
[
|
|
23
|
+
('https://example.com/path?q=1#frag', True),
|
|
24
|
+
('//example.com/path', True),
|
|
25
|
+
('mailto:someone@example.com', True),
|
|
26
|
+
('http://', True),
|
|
27
|
+
('//user:pass@example.com', True),
|
|
28
|
+
('/absolute/path', False),
|
|
29
|
+
('relative/path', False),
|
|
30
|
+
('../parent', False),
|
|
31
|
+
('#fragment', False),
|
|
32
|
+
('?query=val', False),
|
|
33
|
+
('', False),
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
@staticmethod
|
|
37
|
+
def test_absolute_returns_true(uri, is_absolute):
|
|
38
|
+
assert is_uri_absolute(uri) == is_absolute
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize(
|
|
41
|
+
('uri', 'is_absolute'), [('https://a.b', True), ('/x', False)],
|
|
42
|
+
)
|
|
43
|
+
@staticmethod
|
|
44
|
+
def test_accepts_pre_parsed_split_result(uri, is_absolute):
|
|
45
|
+
assert is_uri_absolute(urlsplit(uri)) == is_absolute
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestRPURIBValidateURIBaseSplit:
|
|
49
|
+
|
|
50
|
+
_VALID_BASE = urlsplit('https://example.com/base')
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def test_returns_input_for_valid_absolute_base(cls):
|
|
54
|
+
result = rpurib_validate_uri_base_split(cls._VALID_BASE)
|
|
55
|
+
assert result == cls._VALID_BASE
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize(
|
|
58
|
+
('uri_base_split', 'code_exp'),
|
|
59
|
+
[
|
|
60
|
+
(
|
|
61
|
+
_VALID_BASE._replace(scheme='', netloc=''),
|
|
62
|
+
RPURIBValidateURIValueErrorCode.IS_RELATIVE,
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
_VALID_BASE._replace(query='a=1'),
|
|
66
|
+
RPURIBValidateURIValueErrorCode.HAS_QUERY,
|
|
67
|
+
),
|
|
68
|
+
(
|
|
69
|
+
_VALID_BASE._replace(fragment='section-1'),
|
|
70
|
+
RPURIBValidateURIValueErrorCode.HAS_FRAGMENT,
|
|
71
|
+
),
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
@staticmethod
|
|
75
|
+
def test_rpurib_validate_uri_base_split_rejects_invalid_base_uris(
|
|
76
|
+
uri_base_split, code_exp,
|
|
77
|
+
):
|
|
78
|
+
with pytest.raises(RPURIBValidateURIValueError) as exc_info:
|
|
79
|
+
rpurib_validate_uri_base_split(uri_base_split)
|
|
80
|
+
assert exc_info.value.code == code_exp
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestRPURIBValidateURISplit:
|
|
84
|
+
|
|
85
|
+
_VALID_URI = urlsplit('https://example.com/path')
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def test_returns_input_for_valid_absolute_uri(cls):
|
|
89
|
+
result = rpurib_validate_uri_split(cls._VALID_URI)
|
|
90
|
+
assert result == cls._VALID_URI
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize(
|
|
93
|
+
('uri_split', 'code_exp'),
|
|
94
|
+
[
|
|
95
|
+
(
|
|
96
|
+
_VALID_URI._replace(scheme='', netloc=''),
|
|
97
|
+
RPURIBValidateURIValueErrorCode.IS_RELATIVE,
|
|
98
|
+
),
|
|
99
|
+
(
|
|
100
|
+
_VALID_URI._replace(query='a=1'),
|
|
101
|
+
RPURIBValidateURIValueErrorCode.HAS_QUERY,
|
|
102
|
+
),
|
|
103
|
+
(
|
|
104
|
+
_VALID_URI._replace(fragment='section-1'),
|
|
105
|
+
RPURIBValidateURIValueErrorCode.HAS_FRAGMENT,
|
|
106
|
+
),
|
|
107
|
+
],
|
|
108
|
+
)
|
|
109
|
+
@staticmethod
|
|
110
|
+
def test_rpurib_validate_uri_split_rejects_invalid_uris(
|
|
111
|
+
uri_split, code_exp,
|
|
112
|
+
):
|
|
113
|
+
with pytest.raises(RPURIBValidateURIValueError) as exc_info:
|
|
114
|
+
rpurib_validate_uri_split(uri_split)
|
|
115
|
+
assert exc_info.value.code == code_exp
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestRelativePathFromURIByBaseSplitsValidated:
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize(
|
|
121
|
+
('uri', 'base', 'expected'),
|
|
122
|
+
[
|
|
123
|
+
('https://ex.com/a/b/c', 'https://ex.com/a/b/', PurePosixPath('c')),
|
|
124
|
+
('https://ex.com/a/b/c/d', 'https://ex.com/a/b/', PurePosixPath('c/d')),
|
|
125
|
+
('https://ex.com/a/b', 'https://ex.com/a/b', PurePosixPath('.')),
|
|
126
|
+
('https://ex.com/a/b/', 'https://ex.com/a/b', PurePosixPath('.')),
|
|
127
|
+
('https://ex.com/', 'https://ex.com/', PurePosixPath('.')),
|
|
128
|
+
('https://ex.com/x', 'https://ex.com/', PurePosixPath('x')),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
@staticmethod
|
|
132
|
+
def test_relative_path_success(uri, base, expected):
|
|
133
|
+
assert (
|
|
134
|
+
relative_path_from_uri_by_base_splits_validated(
|
|
135
|
+
urlsplit(uri), urlsplit(base),
|
|
136
|
+
)
|
|
137
|
+
== expected
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def test_raises_not_relative():
|
|
142
|
+
with pytest.raises(RPURIBNotRelativeValueError):
|
|
143
|
+
relative_path_from_uri_by_base_splits_validated(
|
|
144
|
+
urlsplit('https://ex.com/a/b/'), urlsplit('https://ex.com/a/c/d'),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_uri_not_under_old_base_propagates_error():
|
|
149
|
+
with pytest.raises(RPURIBNotRelativeValueError):
|
|
150
|
+
translate_uri_base_splits_validated(
|
|
151
|
+
urlsplit('https://old.example.com/other/file'),
|
|
152
|
+
urlsplit('https://old.example.com/base/'),
|
|
153
|
+
urlsplit('https://new.example.com/base/'),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.parametrize(
|
|
158
|
+
('uri', 'old_base', 'new_base', 'expected'),
|
|
159
|
+
[
|
|
160
|
+
(
|
|
161
|
+
'https://old.example.com/base/file.txt',
|
|
162
|
+
'https://old.example.com/base/',
|
|
163
|
+
'https://new.example.com/newbase/',
|
|
164
|
+
'https://new.example.com/newbase/file.txt',
|
|
165
|
+
),
|
|
166
|
+
(
|
|
167
|
+
'https://old.example.com/base/sub/deep/file.txt',
|
|
168
|
+
'https://old.example.com/base/',
|
|
169
|
+
'https://new.example.com/newbase/',
|
|
170
|
+
'https://new.example.com/newbase/sub/deep/file.txt',
|
|
171
|
+
),
|
|
172
|
+
(
|
|
173
|
+
'https://old.example.com/base/file',
|
|
174
|
+
'https://old.example.com/base/',
|
|
175
|
+
'http://new.example.com/base/',
|
|
176
|
+
'http://new.example.com/base/file',
|
|
177
|
+
),
|
|
178
|
+
(
|
|
179
|
+
'https://old.example.com/base/file',
|
|
180
|
+
'https://old.example.com/base/',
|
|
181
|
+
'https://new-domain.com/base/',
|
|
182
|
+
'https://new-domain.com/base/file',
|
|
183
|
+
),
|
|
184
|
+
(
|
|
185
|
+
'https://old.example.com/base/file',
|
|
186
|
+
'https://old.example.com/base/',
|
|
187
|
+
'https://user:pass@new.example.com/base/',
|
|
188
|
+
'https://user:pass@new.example.com/base/file',
|
|
189
|
+
),
|
|
190
|
+
(
|
|
191
|
+
'https://old.example.com/base/file',
|
|
192
|
+
'https://old.example.com/base/',
|
|
193
|
+
'https://new.example.com:8080/base/',
|
|
194
|
+
'https://new.example.com:8080/base/file',
|
|
195
|
+
),
|
|
196
|
+
(
|
|
197
|
+
'https://old.example.com/base/file',
|
|
198
|
+
'https://old.example.com/base/',
|
|
199
|
+
'https://new.example.com/a/b/c/base/',
|
|
200
|
+
'https://new.example.com/a/b/c/base/file',
|
|
201
|
+
),
|
|
202
|
+
],
|
|
203
|
+
)
|
|
204
|
+
def test_translation_uri_base_splits_validated(uri, old_base, new_base, expected):
|
|
205
|
+
assert (
|
|
206
|
+
urlunsplit(translate_uri_base_splits_validated(
|
|
207
|
+
urlsplit(uri), urlsplit(old_base), urlsplit(new_base),
|
|
208
|
+
))
|
|
209
|
+
== expected
|
|
210
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from pathlib import PurePosixPath
|
|
3
|
+
from urllib.parse import SplitResult, unquote, urlsplit
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_uri_absolute(uri: str | SplitResult) -> bool:
|
|
7
|
+
# here scheme is a default value:
|
|
8
|
+
if not isinstance(uri, SplitResult):
|
|
9
|
+
uri_split = urlsplit(uri, scheme='')
|
|
10
|
+
else:
|
|
11
|
+
uri_split = uri
|
|
12
|
+
return (
|
|
13
|
+
bool(uri_split.scheme)
|
|
14
|
+
or bool(uri_split.netloc)
|
|
15
|
+
or (uri_split.username is not None)
|
|
16
|
+
or (uri_split.password is not None)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RPURIBValidateURIValueErrorCode(StrEnum):
|
|
21
|
+
IS_RELATIVE = 'is_relative'
|
|
22
|
+
HAS_QUERY = 'has_query'
|
|
23
|
+
HAS_FRAGMENT = 'has_fragment'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RPURIBValidateURIValueError(ValueError):
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str, code: RPURIBValidateURIValueErrorCode):
|
|
29
|
+
super().__init__(message, code)
|
|
30
|
+
self.code = code
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def rpurib_validate_uri_base_split(uri_base_split: SplitResult) -> SplitResult:
|
|
34
|
+
if not is_uri_absolute(uri_base_split):
|
|
35
|
+
raise RPURIBValidateURIValueError(
|
|
36
|
+
'base URI should be absolute',
|
|
37
|
+
RPURIBValidateURIValueErrorCode.IS_RELATIVE,
|
|
38
|
+
)
|
|
39
|
+
if unquote(uri_base_split.query):
|
|
40
|
+
raise RPURIBValidateURIValueError(
|
|
41
|
+
'base URI should not have a query',
|
|
42
|
+
RPURIBValidateURIValueErrorCode.HAS_QUERY,
|
|
43
|
+
)
|
|
44
|
+
if uri_base_split.fragment:
|
|
45
|
+
raise RPURIBValidateURIValueError(
|
|
46
|
+
'base URI should not have a fragment',
|
|
47
|
+
RPURIBValidateURIValueErrorCode.HAS_FRAGMENT,
|
|
48
|
+
)
|
|
49
|
+
return uri_base_split
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def rpurib_split_and_validate_uri_base(uri_base: str) -> SplitResult:
|
|
53
|
+
# here scheme is a default value:
|
|
54
|
+
uri_base_split = urlsplit(uri_base, scheme='')
|
|
55
|
+
return rpurib_validate_uri_base_split(uri_base_split)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def rpurib_validate_uri_split(uri_split: SplitResult) -> SplitResult:
|
|
59
|
+
if not is_uri_absolute(uri_split):
|
|
60
|
+
raise RPURIBValidateURIValueError(
|
|
61
|
+
'URI should be absolute',
|
|
62
|
+
RPURIBValidateURIValueErrorCode.IS_RELATIVE,
|
|
63
|
+
)
|
|
64
|
+
if uri_split.fragment:
|
|
65
|
+
raise RPURIBValidateURIValueError(
|
|
66
|
+
'URI should not have a fragment',
|
|
67
|
+
RPURIBValidateURIValueErrorCode.HAS_FRAGMENT,
|
|
68
|
+
)
|
|
69
|
+
if unquote(uri_split.query):
|
|
70
|
+
raise RPURIBValidateURIValueError(
|
|
71
|
+
'URI should not have a query',
|
|
72
|
+
RPURIBValidateURIValueErrorCode.HAS_QUERY,
|
|
73
|
+
)
|
|
74
|
+
return uri_split
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def rpurib_split_and_validate_uri(uri: str) -> SplitResult:
|
|
78
|
+
# here scheme is a default value:
|
|
79
|
+
uri_split = urlsplit(uri, scheme='')
|
|
80
|
+
return rpurib_validate_uri_split(uri_split)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class RPURIBNotRelativeValueError(ValueError):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def relative_path_from_uri_by_base_splits_validated(
|
|
88
|
+
uri_split: SplitResult, uri_base_split: SplitResult,
|
|
89
|
+
) -> PurePosixPath:
|
|
90
|
+
uri_path = PurePosixPath(unquote(uri_split.path))
|
|
91
|
+
uri_base_path = PurePosixPath(unquote(uri_base_split.path))
|
|
92
|
+
try:
|
|
93
|
+
return uri_path.relative_to(uri_base_path, walk_up=False)
|
|
94
|
+
except ValueError as err:
|
|
95
|
+
raise RPURIBNotRelativeValueError(str(err)) from err
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _relative_path_from_uri_by_base(
|
|
99
|
+
uri: str | SplitResult, uri_base: str | SplitResult,
|
|
100
|
+
) -> PurePosixPath:
|
|
101
|
+
# XXX: ValueError s are propagated
|
|
102
|
+
if not isinstance(uri_base, SplitResult):
|
|
103
|
+
uri_base_split = rpurib_split_and_validate_uri_base(uri_base)
|
|
104
|
+
else:
|
|
105
|
+
uri_base_split = uri_base
|
|
106
|
+
if not isinstance(uri, SplitResult):
|
|
107
|
+
uri_split = rpurib_split_and_validate_uri(uri)
|
|
108
|
+
else:
|
|
109
|
+
uri_split = uri
|
|
110
|
+
return relative_path_from_uri_by_base_splits_validated(uri_split, uri_base_split)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def translate_uri_base_splits_validated(
|
|
114
|
+
uri_split: SplitResult,
|
|
115
|
+
uri_base_old_split: SplitResult,
|
|
116
|
+
uri_base_new_split: SplitResult,
|
|
117
|
+
) -> SplitResult:
|
|
118
|
+
path_rel = _relative_path_from_uri_by_base(uri_split, uri_base_old_split)
|
|
119
|
+
uri_base_new_split = rpurib_validate_uri_base_split(uri_base_new_split)
|
|
120
|
+
path_new = PurePosixPath(uri_base_new_split.path) / path_rel
|
|
121
|
+
return uri_split._replace(
|
|
122
|
+
scheme=uri_base_new_split.scheme,
|
|
123
|
+
netloc=uri_base_new_split.netloc,
|
|
124
|
+
path=str(path_new),
|
|
125
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Utilities for working with format checkers, and some bundled
|
|
2
|
+
format checkers.
|
|
3
|
+
|
|
4
|
+
To use some of the bundled format checkers:
|
|
5
|
+
|
|
6
|
+
1. instantiate a :class:`jsonschema.FormatChecker`
|
|
7
|
+
(or use one which you already instantiate);
|
|
8
|
+
2. add desired format checking functions to the format checker
|
|
9
|
+
using :func:`register_funcs_in_checker` or :func:`register_func_in_checker`
|
|
10
|
+
with bundled instances of :class:`FormatCheckingFuncInfo`;
|
|
11
|
+
3. pass the format checker to :func:`jsonschema.validate`
|
|
12
|
+
or :class:`jsonschema.protocols.Validator` to enable validation of formats.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ._common import * # noqa: F401,F403
|
|
16
|
+
from .numbers_range import * # noqa: F401,F403
|
|
17
|
+
from .slice_string import * # noqa: F401,F403
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = (
|
|
21
|
+
'FormatCheckFn',
|
|
22
|
+
'FormatCheckingFuncInfo',
|
|
23
|
+
'register_func_in_checker',
|
|
24
|
+
'register_funcs_in_checker',
|
|
25
|
+
'is_numbers_range',
|
|
26
|
+
'is_numbers_range_info',
|
|
27
|
+
'SLICE_STRING_PATTERN',
|
|
28
|
+
'is_slice_string',
|
|
29
|
+
'is_slice_string_info',
|
|
30
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
2
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from jsonschema import FormatChecker
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
#: Format checking function.
|
|
10
|
+
#:
|
|
11
|
+
#: Note:
|
|
12
|
+
#: Any exceptions the function raises on an invalid data value are part
|
|
13
|
+
#: of the function's contract for :class:`~jsonschema.FormatChecker`.
|
|
14
|
+
#:
|
|
15
|
+
#: Args:
|
|
16
|
+
#: object: a data value to validate
|
|
17
|
+
#:
|
|
18
|
+
#: Returns:
|
|
19
|
+
#: bool: whether the object is valid for the format
|
|
20
|
+
type FormatCheckFn = Callable[[object], bool]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FormatCheckingFuncInfo(NamedTuple):
|
|
24
|
+
"""Data needed to register a format checking function
|
|
25
|
+
in a :class:`~jsonschema.FormatChecker`.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
format: Exact name of the format for JSON Schema.
|
|
29
|
+
func:
|
|
30
|
+
Function that checks if a JSON data value satisfies a format.
|
|
31
|
+
raises:
|
|
32
|
+
Type(s) of exceptions raised by `func` on an invalid value.
|
|
33
|
+
Exceptions of other types are immediately propagated.
|
|
34
|
+
See :meth:`~jsonschema.FormatChecker.checks` for details.
|
|
35
|
+
"""
|
|
36
|
+
format: str
|
|
37
|
+
func: FormatCheckFn
|
|
38
|
+
raises: type[Exception] | tuple[type[Exception], ...] = ()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register_func_in_checker(
|
|
42
|
+
checker: 'FormatChecker', func_info: FormatCheckingFuncInfo,
|
|
43
|
+
) -> FormatCheckFn:
|
|
44
|
+
"""Register a format checking function
|
|
45
|
+
in a :class:`~jsonschema.FormatChecker`.
|
|
46
|
+
|
|
47
|
+
Utility function working with :class:`FormatCheckingFuncInfo`.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
checker (jsonschema.FormatChecker): A ``format`` property checker.
|
|
51
|
+
func_info (FormatCheckingFuncInfo):
|
|
52
|
+
Data needed to register a format checking function.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
``func_info.func``
|
|
56
|
+
"""
|
|
57
|
+
return checker.checks(func_info.format, func_info.raises)(func_info.func)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def register_funcs_in_checker(
|
|
61
|
+
checker: 'FormatChecker', funcs_info: Iterable[FormatCheckingFuncInfo],
|
|
62
|
+
) -> Sequence[FormatCheckFn]:
|
|
63
|
+
"""Register multiple format checking functions
|
|
64
|
+
in a :class:`~jsonschema.FormatChecker`.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
checker (jsonschema.FormatChecker): A ``format`` property checker.
|
|
68
|
+
funcs_info (Iterable[FormatCheckingFuncInfo]):
|
|
69
|
+
Instances of data needed to register formatt checking functions.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
sequence of ``func_info.func`` objects
|
|
73
|
+
"""
|
|
74
|
+
return [register_func_in_checker(checker, func_info) for func_info in funcs_info]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from jsonschema_extras._util import SEQUENCE_PRIMITIVE_TYPES_DEFAULT
|
|
5
|
+
from ._common import FormatCheckingFuncInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_numbers_range(instance: object) -> bool:
|
|
9
|
+
"""Tests if an object is a numbers range (a sequence of 2 numbers
|
|
10
|
+
in non-descending order).
|
|
11
|
+
|
|
12
|
+
A numbers range is defined as a sequence containing exactly 2 comparable
|
|
13
|
+
numeric elements where the first element is less than or equal
|
|
14
|
+
to the second element, forming a valid range [min, max].
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
bool: Whether `instance` is a numbers range: ascending sequence
|
|
18
|
+
of 2 numbers.
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
|
|
22
|
+
Valid ranges::
|
|
23
|
+
|
|
24
|
+
>>> is_numbers_range([1, 5])
|
|
25
|
+
True
|
|
26
|
+
>>> is_numbers_range([3.14, 3.14])
|
|
27
|
+
True
|
|
28
|
+
>>> is_numbers_range((0, 10))
|
|
29
|
+
True
|
|
30
|
+
|
|
31
|
+
Invalid ranges::
|
|
32
|
+
|
|
33
|
+
>>> is_numbers_range([5, 1])
|
|
34
|
+
False
|
|
35
|
+
>>> is_numbers_range([1, 2, 3])
|
|
36
|
+
False
|
|
37
|
+
>>> is_numbers_range([1])
|
|
38
|
+
False
|
|
39
|
+
>>> is_numbers_range(5)
|
|
40
|
+
False
|
|
41
|
+
"""
|
|
42
|
+
if not (
|
|
43
|
+
isinstance(instance, Sequence)
|
|
44
|
+
and (not isinstance(instance, tuple(SEQUENCE_PRIMITIVE_TYPES_DEFAULT)))
|
|
45
|
+
and (len(instance) == 2)
|
|
46
|
+
):
|
|
47
|
+
return False
|
|
48
|
+
try:
|
|
49
|
+
return cast(bool, (instance[0] <= instance[1]))
|
|
50
|
+
except TypeError:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
#: Format ``numbers-range`` checked by :func:`is_numbers_range`
|
|
55
|
+
is_numbers_range_info = FormatCheckingFuncInfo(
|
|
56
|
+
'numbers-range', is_numbers_range, (TypeError,),
|
|
57
|
+
)
|