jsonschema-extras 0.2.0__tar.gz

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.
Files changed (38) hide show
  1. jsonschema_extras-0.2.0/LICENSE.txt +21 -0
  2. jsonschema_extras-0.2.0/MANIFEST.in +2 -0
  3. jsonschema_extras-0.2.0/PKG-INFO +52 -0
  4. jsonschema_extras-0.2.0/README.md +28 -0
  5. jsonschema_extras-0.2.0/jsonschema_extras/VERSION +1 -0
  6. jsonschema_extras-0.2.0/jsonschema_extras/__init__.py +96 -0
  7. jsonschema_extras-0.2.0/jsonschema_extras/_common.py +31 -0
  8. jsonschema_extras-0.2.0/jsonschema_extras/_util/__init__.py +21 -0
  9. jsonschema_extras-0.2.0/jsonschema_extras/_util/tests/__init__.py +0 -0
  10. jsonschema_extras-0.2.0/jsonschema_extras/_util/tests/test_uri.py +210 -0
  11. jsonschema_extras-0.2.0/jsonschema_extras/_util/uri.py +125 -0
  12. jsonschema_extras-0.2.0/jsonschema_extras/formats/__init__.py +30 -0
  13. jsonschema_extras-0.2.0/jsonschema_extras/formats/_common.py +74 -0
  14. jsonschema_extras-0.2.0/jsonschema_extras/formats/numbers_range.py +57 -0
  15. jsonschema_extras-0.2.0/jsonschema_extras/formats/slice_string.py +69 -0
  16. jsonschema_extras-0.2.0/jsonschema_extras/formats/tests/__init__.py +0 -0
  17. jsonschema_extras-0.2.0/jsonschema_extras/formats/tests/test_numbers_range.py +22 -0
  18. jsonschema_extras-0.2.0/jsonschema_extras/formats/tests/test_slice_string.py +39 -0
  19. jsonschema_extras-0.2.0/jsonschema_extras/registries/__init__.py +25 -0
  20. jsonschema_extras-0.2.0/jsonschema_extras/registries/_common.py +209 -0
  21. jsonschema_extras-0.2.0/jsonschema_extras/registries/filesystem.py +330 -0
  22. jsonschema_extras-0.2.0/jsonschema_extras/registries/retrieval/__init__.py +184 -0
  23. jsonschema_extras-0.2.0/jsonschema_extras/registries/retrieval/_common.py +14 -0
  24. jsonschema_extras-0.2.0/jsonschema_extras/registries/retrieval/json.py +18 -0
  25. jsonschema_extras-0.2.0/jsonschema_extras/registries/tests/__init__.py +0 -0
  26. jsonschema_extras-0.2.0/jsonschema_extras/registries/tests/test_filesystem.py +131 -0
  27. jsonschema_extras-0.2.0/jsonschema_extras/schemas/common/range.json +15 -0
  28. jsonschema_extras-0.2.0/jsonschema_extras/schemas/common/range_integer.json +12 -0
  29. jsonschema_extras-0.2.0/jsonschema_extras/schemas/common/slice_object.json +29 -0
  30. jsonschema_extras-0.2.0/jsonschema_extras/schemas/common/slice_string.json +21 -0
  31. jsonschema_extras-0.2.0/jsonschema_extras/typing.py +4 -0
  32. jsonschema_extras-0.2.0/jsonschema_extras.egg-info/PKG-INFO +52 -0
  33. jsonschema_extras-0.2.0/jsonschema_extras.egg-info/SOURCES.txt +36 -0
  34. jsonschema_extras-0.2.0/jsonschema_extras.egg-info/dependency_links.txt +1 -0
  35. jsonschema_extras-0.2.0/jsonschema_extras.egg-info/requires.txt +2 -0
  36. jsonschema_extras-0.2.0/jsonschema_extras.egg-info/top_level.txt +1 -0
  37. jsonschema_extras-0.2.0/pyproject.toml +103 -0
  38. jsonschema_extras-0.2.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Viacheslav Syropiatov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include jsonschema_extras/VERSION
2
+ recursive-include jsonschema_extras/schemas *.json
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: jsonschema-extras
3
+ Version: 0.2.0
4
+ Summary: Utilities for working with the Python library 'jsonschema'
5
+ Author-email: Viacheslav Syropiatov <nonexistent@example.com>
6
+ License-Expression: MIT
7
+ Project-URL: source, https://github.com/asrelo/jsonschema-extras
8
+ Project-URL: documentation, https://asrelo.github.io/jsonschema-extras
9
+ Project-URL: issues, https://github.com/asrelo/jsonschema-extras/issues
10
+ Keywords: json,json schema,jsonschema,validation,extras,utilities,utils
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.12
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE.txt
21
+ Requires-Dist: jsonschema~=4.26.0
22
+ Requires-Dist: types-jsonschema
23
+ Dynamic: license-file
24
+
25
+ # jsonschema-extras
26
+
27
+ [![PyPI version](https://img.shields.io/pypi/v/jsonschema-extras.svg)](https://pypi.org/project/jsonschema-extras)
28
+ [![Python versions](https://img.shields.io/pypi/pyversions/jsonschema-extras.svg)](https://pypi.org/project/jsonschema-extras)
29
+ [![License](https://img.shields.io/github/license/asrelo/jsonschema-extras.svg)](https://github.com/asrelo/jsonschema-extras/blob/main/LICENSE.txt)
30
+
31
+ Utilities for working with the JSON Schema Python library [`jsonschema`](https://pypi.org/project/jsonschema/).
32
+
33
+ **See [documentation](https://asrelo.github.io/jsonschema-extras).**
34
+
35
+ **Contributions are welcome**, but please consider discussing it in [GitHub issues](https://github.com/asrelo/jsonschema-extras/issues) before committing to it.
36
+
37
+ ## Features
38
+
39
+ - Specific schemas bundled with the library (easily usable)
40
+ - Specific formats bundled with the library (easily usable)
41
+ - Utilities for accessing JSON-encoded schemas on a local filesystem
42
+ - Other utilities
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install jsonschema-extras
48
+ ```
49
+
50
+ ## License
51
+
52
+ This project is licensed under the [MIT License](LICENSE.txt).
@@ -0,0 +1,28 @@
1
+ # jsonschema-extras
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/jsonschema-extras.svg)](https://pypi.org/project/jsonschema-extras)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/jsonschema-extras.svg)](https://pypi.org/project/jsonschema-extras)
5
+ [![License](https://img.shields.io/github/license/asrelo/jsonschema-extras.svg)](https://github.com/asrelo/jsonschema-extras/blob/main/LICENSE.txt)
6
+
7
+ Utilities for working with the JSON Schema Python library [`jsonschema`](https://pypi.org/project/jsonschema/).
8
+
9
+ **See [documentation](https://asrelo.github.io/jsonschema-extras).**
10
+
11
+ **Contributions are welcome**, but please consider discussing it in [GitHub issues](https://github.com/asrelo/jsonschema-extras/issues) before committing to it.
12
+
13
+ ## Features
14
+
15
+ - Specific schemas bundled with the library (easily usable)
16
+ - Specific formats bundled with the library (easily usable)
17
+ - Utilities for accessing JSON-encoded schemas on a local filesystem
18
+ - Other utilities
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install jsonschema-extras
24
+ ```
25
+
26
+ ## License
27
+
28
+ This project is licensed under the [MIT License](LICENSE.txt).
@@ -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())
@@ -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
+ )