jsonschema-path 0.3.4__tar.gz → 0.4.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.
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/PKG-INFO +46 -8
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/README.rst +38 -1
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/__init__.py +1 -1
- jsonschema_path-0.4.0/jsonschema_path/accessors.py +218 -0
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/file.py +3 -5
- jsonschema_path-0.4.0/jsonschema_path/handlers/protocols.py +5 -0
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/requests.py +2 -4
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/urllib.py +1 -2
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/loaders.py +3 -5
- jsonschema_path-0.4.0/jsonschema_path/paths.py +254 -0
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/readers.py +4 -7
- jsonschema_path-0.4.0/jsonschema_path/typing.py +24 -0
- jsonschema_path-0.4.0/jsonschema_path/utils.py +9 -0
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/pyproject.toml +43 -16
- jsonschema_path-0.3.4/jsonschema_path/accessors.py +0 -95
- jsonschema_path-0.3.4/jsonschema_path/handlers/protocols.py +0 -6
- jsonschema_path-0.3.4/jsonschema_path/paths.py +0 -133
- jsonschema_path-0.3.4/jsonschema_path/typing.py +0 -8
- jsonschema_path-0.3.4/jsonschema_path/utils.py +0 -8
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/LICENSE +0 -0
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/__init__.py +0 -0
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/utils.py +0 -0
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/py.typed +0 -0
- {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/retrievers.py +0 -0
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: jsonschema-path
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: JSONSchema Spec with object-oriented paths
|
|
5
5
|
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: jsonschema,swagger,spec
|
|
7
8
|
Author: Artur Maciag
|
|
8
9
|
Author-email: maciag.artur@gmail.com
|
|
9
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.10,<4.0.0
|
|
10
11
|
Classifier: Development Status :: 4 - Beta
|
|
11
12
|
Classifier: Intended Audience :: Developers
|
|
12
13
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Provides-Extra: requests
|
|
23
24
|
Requires-Dist: PyYAML (>=5.1)
|
|
24
|
-
Requires-Dist: pathable (>=0.
|
|
25
|
+
Requires-Dist: pathable (>=0.5.0,<0.6.0)
|
|
25
26
|
Requires-Dist: referencing (<0.37.0)
|
|
26
|
-
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
27
|
+
Requires-Dist: requests (>=2.31.0,<3.0.0) ; extra == "requests"
|
|
27
28
|
Project-URL: Repository, https://github.com/p1c2u/jsonschema-path
|
|
28
29
|
Description-Content-Type: text/x-rst
|
|
29
30
|
|
|
@@ -119,6 +120,43 @@ Usage
|
|
|
119
120
|
{'type': 'string', 'default': '1.0'}
|
|
120
121
|
|
|
121
122
|
|
|
123
|
+
Benchmarks
|
|
124
|
+
##########
|
|
125
|
+
|
|
126
|
+
Benchmarks mirror the lightweight (dependency-free) JSON output format used in
|
|
127
|
+
`pathable`.
|
|
128
|
+
|
|
129
|
+
Run locally with Poetry:
|
|
130
|
+
|
|
131
|
+
.. code-block:: console
|
|
132
|
+
|
|
133
|
+
poetry run python -m tests.benchmarks.bench_parse --output reports/bench-parse.json
|
|
134
|
+
poetry run python -m tests.benchmarks.bench_lookup --output reports/bench-lookup.json
|
|
135
|
+
|
|
136
|
+
For a quick smoke run:
|
|
137
|
+
|
|
138
|
+
.. code-block:: console
|
|
139
|
+
|
|
140
|
+
poetry run python -m tests.benchmarks.bench_parse --output reports/bench-parse.quick.json --quick
|
|
141
|
+
poetry run python -m tests.benchmarks.bench_lookup --output reports/bench-lookup.quick.json --quick
|
|
142
|
+
|
|
143
|
+
You can also control repeats/warmup via env vars:
|
|
144
|
+
|
|
145
|
+
.. code-block:: console
|
|
146
|
+
|
|
147
|
+
export JSONSCHEMA_PATH_BENCH_REPEATS=5
|
|
148
|
+
export JSONSCHEMA_PATH_BENCH_WARMUP=1
|
|
149
|
+
|
|
150
|
+
Compare two results:
|
|
151
|
+
|
|
152
|
+
.. code-block:: console
|
|
153
|
+
|
|
154
|
+
poetry run python -m tests.benchmarks.compare_results \
|
|
155
|
+
--baseline reports/bench-lookup-master.json \
|
|
156
|
+
--candidate reports/bench-lookup.json \
|
|
157
|
+
--tolerance 0.20
|
|
158
|
+
|
|
159
|
+
|
|
122
160
|
Related projects
|
|
123
161
|
################
|
|
124
162
|
|
|
@@ -132,5 +170,5 @@ Related projects
|
|
|
132
170
|
License
|
|
133
171
|
#######
|
|
134
172
|
|
|
135
|
-
Copyright (c) 2017-
|
|
173
|
+
Copyright (c) 2017-2025, Artur Maciag, All rights reserved. Apache-2.0
|
|
136
174
|
|
|
@@ -90,6 +90,43 @@ Usage
|
|
|
90
90
|
{'type': 'string', 'default': '1.0'}
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
Benchmarks
|
|
94
|
+
##########
|
|
95
|
+
|
|
96
|
+
Benchmarks mirror the lightweight (dependency-free) JSON output format used in
|
|
97
|
+
`pathable`.
|
|
98
|
+
|
|
99
|
+
Run locally with Poetry:
|
|
100
|
+
|
|
101
|
+
.. code-block:: console
|
|
102
|
+
|
|
103
|
+
poetry run python -m tests.benchmarks.bench_parse --output reports/bench-parse.json
|
|
104
|
+
poetry run python -m tests.benchmarks.bench_lookup --output reports/bench-lookup.json
|
|
105
|
+
|
|
106
|
+
For a quick smoke run:
|
|
107
|
+
|
|
108
|
+
.. code-block:: console
|
|
109
|
+
|
|
110
|
+
poetry run python -m tests.benchmarks.bench_parse --output reports/bench-parse.quick.json --quick
|
|
111
|
+
poetry run python -m tests.benchmarks.bench_lookup --output reports/bench-lookup.quick.json --quick
|
|
112
|
+
|
|
113
|
+
You can also control repeats/warmup via env vars:
|
|
114
|
+
|
|
115
|
+
.. code-block:: console
|
|
116
|
+
|
|
117
|
+
export JSONSCHEMA_PATH_BENCH_REPEATS=5
|
|
118
|
+
export JSONSCHEMA_PATH_BENCH_WARMUP=1
|
|
119
|
+
|
|
120
|
+
Compare two results:
|
|
121
|
+
|
|
122
|
+
.. code-block:: console
|
|
123
|
+
|
|
124
|
+
poetry run python -m tests.benchmarks.compare_results \
|
|
125
|
+
--baseline reports/bench-lookup-master.json \
|
|
126
|
+
--candidate reports/bench-lookup.json \
|
|
127
|
+
--tolerance 0.20
|
|
128
|
+
|
|
129
|
+
|
|
93
130
|
Related projects
|
|
94
131
|
################
|
|
95
132
|
|
|
@@ -103,4 +140,4 @@ Related projects
|
|
|
103
140
|
License
|
|
104
141
|
#######
|
|
105
142
|
|
|
106
|
-
Copyright (c) 2017-
|
|
143
|
+
Copyright (c) 2017-2025, Artur Maciag, All rights reserved. Apache-2.0
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""JSONSchema spec accessors module."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Hashable
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import Any
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from pathable.accessors import LookupAccessor
|
|
11
|
+
from pathable.types import LookupKey
|
|
12
|
+
from pathable.types import LookupNode
|
|
13
|
+
from pathable.types import LookupValue
|
|
14
|
+
from referencing import Registry
|
|
15
|
+
from referencing import Specification
|
|
16
|
+
from referencing._core import Resolved
|
|
17
|
+
from referencing._core import Resolver
|
|
18
|
+
from referencing.jsonschema import DRAFT202012
|
|
19
|
+
|
|
20
|
+
from jsonschema_path.handlers import default_handlers
|
|
21
|
+
from jsonschema_path.retrievers import SchemaRetriever
|
|
22
|
+
from jsonschema_path.typing import ResolverHandlers
|
|
23
|
+
from jsonschema_path.typing import Schema
|
|
24
|
+
from jsonschema_path.utils import is_ref
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SchemaAccessor(LookupAccessor):
|
|
28
|
+
_resolver_refs: dict[int, Resolver[Schema] | None] = {}
|
|
29
|
+
|
|
30
|
+
def __init__(self, schema: Schema, resolver: Resolver[Schema]):
|
|
31
|
+
super().__init__(cast(LookupNode, schema))
|
|
32
|
+
self.resolver = resolver
|
|
33
|
+
|
|
34
|
+
self._resolver_refs[id(schema)] = resolver
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_schema(
|
|
38
|
+
cls,
|
|
39
|
+
schema: Schema,
|
|
40
|
+
specification: Specification[Schema] = DRAFT202012,
|
|
41
|
+
base_uri: str = "",
|
|
42
|
+
handlers: ResolverHandlers | None = None,
|
|
43
|
+
) -> "SchemaAccessor":
|
|
44
|
+
if handlers is None:
|
|
45
|
+
handlers = default_handlers
|
|
46
|
+
retriever = SchemaRetriever(handlers, specification)
|
|
47
|
+
base_resource = specification.create_resource(schema)
|
|
48
|
+
registry: Registry[Schema] = Registry(
|
|
49
|
+
retrieve=retriever, # type: ignore
|
|
50
|
+
)
|
|
51
|
+
registry = registry.with_resource(base_uri, base_resource)
|
|
52
|
+
resolver = registry.resolver(base_uri=base_uri)
|
|
53
|
+
return cls(schema, resolver)
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, parts: Sequence[LookupKey]) -> LookupNode:
|
|
56
|
+
return self._get_node(self.node, parts, self.resolver)
|
|
57
|
+
|
|
58
|
+
def stat(self, parts: Sequence[Hashable]) -> dict[str, Any] | None:
|
|
59
|
+
try:
|
|
60
|
+
resolved = self.get_resolved(cast(Sequence[LookupKey], parts))
|
|
61
|
+
except (KeyError, IndexError, TypeError):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
node = resolved.contents
|
|
65
|
+
|
|
66
|
+
if self._is_traversable_node(node):
|
|
67
|
+
return {
|
|
68
|
+
"type": type(node).__name__,
|
|
69
|
+
"length": len(node),
|
|
70
|
+
}
|
|
71
|
+
try:
|
|
72
|
+
length = len(cast(Any, node))
|
|
73
|
+
except TypeError:
|
|
74
|
+
length = None
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"type": type(node).__name__,
|
|
78
|
+
"length": length,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def keys(self, parts: Sequence[LookupKey]) -> Sequence[LookupKey]:
|
|
82
|
+
resolved = self.get_resolved(parts)
|
|
83
|
+
node = resolved.contents
|
|
84
|
+
|
|
85
|
+
if isinstance(node, dict):
|
|
86
|
+
# dict_keys has O(1) membership, no allocation.
|
|
87
|
+
return cast(Sequence[LookupKey], node.keys())
|
|
88
|
+
if isinstance(node, list):
|
|
89
|
+
# range has O(1) membership and supports iteration.
|
|
90
|
+
return cast(Sequence[LookupKey], range(len(node)))
|
|
91
|
+
|
|
92
|
+
# Non-traversable leaf.
|
|
93
|
+
if parts:
|
|
94
|
+
raise KeyError(parts[-1])
|
|
95
|
+
raise KeyError
|
|
96
|
+
|
|
97
|
+
def len(self, parts: Sequence[LookupKey]) -> int:
|
|
98
|
+
resolved = self.get_resolved(parts)
|
|
99
|
+
node = resolved.contents
|
|
100
|
+
if isinstance(node, (dict, list)):
|
|
101
|
+
return len(node)
|
|
102
|
+
if parts:
|
|
103
|
+
raise KeyError(parts[-1])
|
|
104
|
+
raise KeyError
|
|
105
|
+
|
|
106
|
+
def contains(self, parts: Sequence[LookupKey], key: LookupKey) -> bool:
|
|
107
|
+
try:
|
|
108
|
+
resolved = self.get_resolved(parts)
|
|
109
|
+
except (KeyError, IndexError, TypeError):
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
node = resolved.contents
|
|
113
|
+
if isinstance(node, dict):
|
|
114
|
+
return key in node
|
|
115
|
+
if isinstance(node, list):
|
|
116
|
+
return isinstance(key, int) and 0 <= key < len(node)
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def require_child(
|
|
120
|
+
self, parts: Sequence[LookupKey], key: LookupKey
|
|
121
|
+
) -> None:
|
|
122
|
+
# Validate parent path for intermediate diagnostics.
|
|
123
|
+
resolved = self.get_resolved(parts)
|
|
124
|
+
node = resolved.contents
|
|
125
|
+
|
|
126
|
+
if isinstance(node, dict):
|
|
127
|
+
if key not in node:
|
|
128
|
+
raise KeyError(key)
|
|
129
|
+
return
|
|
130
|
+
if isinstance(node, list):
|
|
131
|
+
if not (isinstance(key, int) and 0 <= key < len(node)):
|
|
132
|
+
raise KeyError(key)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
raise KeyError(key)
|
|
136
|
+
|
|
137
|
+
def read(self, parts: Sequence[LookupKey]) -> LookupValue:
|
|
138
|
+
resolved = self.get_resolved(parts)
|
|
139
|
+
return self._read_node(resolved.contents)
|
|
140
|
+
|
|
141
|
+
@contextmanager
|
|
142
|
+
def resolve(
|
|
143
|
+
self, parts: Sequence[LookupKey]
|
|
144
|
+
) -> Iterator[Resolved[LookupNode]]:
|
|
145
|
+
try:
|
|
146
|
+
yield self.get_resolved(parts)
|
|
147
|
+
finally:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def get_resolved(self, parts: Sequence[LookupKey]) -> Resolved[LookupNode]:
|
|
151
|
+
resolved = self._get_resolved(self.node, parts, resolver=self.resolver)
|
|
152
|
+
self.resolver = self.resolver._evolve(
|
|
153
|
+
self.resolver._base_uri,
|
|
154
|
+
registry=resolved.resolver._registry,
|
|
155
|
+
)
|
|
156
|
+
return resolved
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def _get_resolved(
|
|
160
|
+
cls,
|
|
161
|
+
node: LookupNode,
|
|
162
|
+
parts: Sequence[LookupKey],
|
|
163
|
+
resolver: Resolver[Schema] | None = None,
|
|
164
|
+
) -> Resolved[LookupNode]:
|
|
165
|
+
if resolver is None:
|
|
166
|
+
raise ValueError("resolver must be provided")
|
|
167
|
+
|
|
168
|
+
current_node: LookupNode = node
|
|
169
|
+
current_resolver: Resolver[Schema] = resolver
|
|
170
|
+
|
|
171
|
+
for part in parts:
|
|
172
|
+
resolved = cls._resolve_node(current_node, current_resolver)
|
|
173
|
+
current_node, current_resolver = (
|
|
174
|
+
resolved.contents,
|
|
175
|
+
resolved.resolver,
|
|
176
|
+
)
|
|
177
|
+
current_node = cls._get_subnode(current_node, part)
|
|
178
|
+
|
|
179
|
+
resolved = cls._resolve_node(current_node, current_resolver)
|
|
180
|
+
return cast(Resolved[LookupNode], resolved)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def _resolve_node(
|
|
184
|
+
cls,
|
|
185
|
+
node: LookupNode,
|
|
186
|
+
resolver: Resolver[Schema],
|
|
187
|
+
) -> Resolved[Schema]:
|
|
188
|
+
if is_ref(node):
|
|
189
|
+
ref_node = cls._get_subnode(node, "$ref")
|
|
190
|
+
ref = cls._read_node(ref_node)
|
|
191
|
+
resolved = resolver.lookup(ref)
|
|
192
|
+
return cls._resolve_node(
|
|
193
|
+
resolved.contents,
|
|
194
|
+
resolved.resolver,
|
|
195
|
+
)
|
|
196
|
+
return Resolved(cast(Schema, node), resolver) # type: ignore
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def _get_node(
|
|
200
|
+
cls,
|
|
201
|
+
node: LookupNode,
|
|
202
|
+
parts: Sequence[LookupKey],
|
|
203
|
+
resolver: Resolver[Schema] | None = None,
|
|
204
|
+
) -> LookupNode:
|
|
205
|
+
if resolver is None:
|
|
206
|
+
raise ValueError("resolver must be provided")
|
|
207
|
+
|
|
208
|
+
current_node: LookupNode = node
|
|
209
|
+
current_resolver: Resolver[Schema] = resolver
|
|
210
|
+
|
|
211
|
+
for part in parts:
|
|
212
|
+
resolved = cls._resolve_node(current_node, current_resolver)
|
|
213
|
+
current_node, current_resolver = (
|
|
214
|
+
resolved.contents,
|
|
215
|
+
resolved.resolver,
|
|
216
|
+
)
|
|
217
|
+
current_node = cls._get_subnode(current_node, part)
|
|
218
|
+
return current_node
|
|
@@ -4,8 +4,6 @@ from json import dumps
|
|
|
4
4
|
from json import loads
|
|
5
5
|
from typing import Any
|
|
6
6
|
from typing import ContextManager
|
|
7
|
-
from typing import Optional
|
|
8
|
-
from typing import Tuple
|
|
9
7
|
from urllib.parse import urlparse
|
|
10
8
|
|
|
11
9
|
from yaml import load
|
|
@@ -32,10 +30,10 @@ class FileHandler:
|
|
|
32
30
|
class BaseFilePathHandler:
|
|
33
31
|
"""Base file path handler."""
|
|
34
32
|
|
|
35
|
-
allowed_schemes:
|
|
33
|
+
allowed_schemes: tuple[str, ...] = NotImplemented
|
|
36
34
|
|
|
37
35
|
def __init__(
|
|
38
|
-
self, *allowed_schemes: str, file_handler:
|
|
36
|
+
self, *allowed_schemes: str, file_handler: FileHandler | None = None
|
|
39
37
|
):
|
|
40
38
|
self.allowed_schemes = allowed_schemes or self.allowed_schemes
|
|
41
39
|
self.file_handler = file_handler or FileHandler()
|
|
@@ -60,7 +58,7 @@ class FilePathHandler(BaseFilePathHandler):
|
|
|
60
58
|
def __init__(
|
|
61
59
|
self,
|
|
62
60
|
*allowed_schemes: str,
|
|
63
|
-
file_handler:
|
|
61
|
+
file_handler: FileHandler | None = None,
|
|
64
62
|
encoding: str = "utf-8",
|
|
65
63
|
):
|
|
66
64
|
super().__init__(*allowed_schemes, file_handler=file_handler)
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
from contextlib import closing
|
|
4
4
|
from io import StringIO
|
|
5
5
|
from typing import ContextManager
|
|
6
|
-
from typing import Optional
|
|
7
|
-
from typing import Union
|
|
8
6
|
|
|
9
7
|
import requests
|
|
10
8
|
|
|
@@ -19,9 +17,9 @@ class UrlRequestsHandler(BaseFilePathHandler):
|
|
|
19
17
|
def __init__(
|
|
20
18
|
self,
|
|
21
19
|
*allowed_schemes: str,
|
|
22
|
-
file_handler:
|
|
20
|
+
file_handler: FileHandler | None = None,
|
|
23
21
|
timeout: int = 10,
|
|
24
|
-
verify:
|
|
22
|
+
verify: bool | str | None = True,
|
|
25
23
|
):
|
|
26
24
|
super().__init__(*allowed_schemes, file_handler=file_handler)
|
|
27
25
|
self.timeout = timeout
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from contextlib import closing
|
|
4
4
|
from typing import ContextManager
|
|
5
|
-
from typing import Optional
|
|
6
5
|
from urllib.request import urlopen
|
|
7
6
|
|
|
8
7
|
from jsonschema_path.handlers.file import BaseFilePathHandler
|
|
@@ -16,7 +15,7 @@ class UrllibHandler(BaseFilePathHandler):
|
|
|
16
15
|
def __init__(
|
|
17
16
|
self,
|
|
18
17
|
*allowed_schemes: str,
|
|
19
|
-
file_handler:
|
|
18
|
+
file_handler: FileHandler | None = None,
|
|
20
19
|
timeout: int = 10
|
|
21
20
|
):
|
|
22
21
|
super().__init__(*allowed_schemes, file_handler=file_handler)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# Use CSafeFile if available
|
|
2
|
+
from collections.abc import Iterable
|
|
2
3
|
from typing import TYPE_CHECKING
|
|
3
4
|
from typing import Any
|
|
4
|
-
from typing import Dict
|
|
5
|
-
from typing import Iterable
|
|
6
|
-
from typing import Tuple
|
|
7
5
|
|
|
8
6
|
if TYPE_CHECKING:
|
|
9
7
|
from yaml import SafeLoader
|
|
@@ -25,8 +23,8 @@ class LimitedSafeLoader(type):
|
|
|
25
23
|
def __new__(
|
|
26
24
|
cls,
|
|
27
25
|
name: str,
|
|
28
|
-
bases:
|
|
29
|
-
namespace:
|
|
26
|
+
bases: tuple[type, ...],
|
|
27
|
+
namespace: dict[str, Any],
|
|
30
28
|
exclude_resolvers: Iterable[str],
|
|
31
29
|
) -> "LimitedSafeLoader":
|
|
32
30
|
exclude_resolvers = set(exclude_resolvers)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""JSONSchema spec paths module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from functools import cached_property
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
from typing import TypeVar
|
|
14
|
+
from typing import overload
|
|
15
|
+
|
|
16
|
+
from pathable import AccessorPath
|
|
17
|
+
from referencing import Specification
|
|
18
|
+
from referencing._core import Resolved
|
|
19
|
+
from referencing.jsonschema import DRAFT202012
|
|
20
|
+
|
|
21
|
+
from jsonschema_path.accessors import SchemaAccessor
|
|
22
|
+
from jsonschema_path.handlers import default_handlers
|
|
23
|
+
from jsonschema_path.handlers.protocols import SupportsRead
|
|
24
|
+
from jsonschema_path.readers import FilePathReader
|
|
25
|
+
from jsonschema_path.readers import FileReader
|
|
26
|
+
from jsonschema_path.readers import PathReader
|
|
27
|
+
from jsonschema_path.typing import ResolverHandlers
|
|
28
|
+
from jsonschema_path.typing import Schema
|
|
29
|
+
from jsonschema_path.typing import SchemaKey
|
|
30
|
+
from jsonschema_path.typing import SchemaNode
|
|
31
|
+
from jsonschema_path.typing import SchemaValue
|
|
32
|
+
from jsonschema_path.typing import is_str_sequence
|
|
33
|
+
|
|
34
|
+
TDefault = TypeVar("TDefault")
|
|
35
|
+
# Python 3.11+ shortcut: typing.Self
|
|
36
|
+
TSchemaPath = TypeVar("TSchemaPath", bound="SchemaPath")
|
|
37
|
+
|
|
38
|
+
SPEC_SEPARATOR = "#"
|
|
39
|
+
NOTSET = object()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SchemaPath(AccessorPath[SchemaNode, SchemaKey, SchemaValue]):
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def _parse_args(
|
|
46
|
+
cls,
|
|
47
|
+
args: Sequence[Any],
|
|
48
|
+
sep: str = SPEC_SEPARATOR,
|
|
49
|
+
) -> tuple[SchemaKey, ...]:
|
|
50
|
+
parts: list[SchemaKey] = []
|
|
51
|
+
append = parts.append
|
|
52
|
+
extend = parts.extend
|
|
53
|
+
|
|
54
|
+
for a in args:
|
|
55
|
+
if isinstance(a, cls):
|
|
56
|
+
extend(a.parts)
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
# Fast-path: benchmarks overwhelmingly pass `str`/`int` parts.
|
|
60
|
+
if isinstance(a, int):
|
|
61
|
+
append(a)
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if isinstance(a, bytes):
|
|
65
|
+
a = a.decode("ascii")
|
|
66
|
+
|
|
67
|
+
if isinstance(a, str):
|
|
68
|
+
if a and a != ".":
|
|
69
|
+
if sep in a:
|
|
70
|
+
for x in a.split(sep):
|
|
71
|
+
if x and x != ".":
|
|
72
|
+
append(x)
|
|
73
|
+
else:
|
|
74
|
+
append(a)
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
# PathLike is relatively expensive to check; keep it after common types.
|
|
78
|
+
if isinstance(a, os.PathLike):
|
|
79
|
+
a = os.fspath(a)
|
|
80
|
+
if isinstance(a, bytes):
|
|
81
|
+
a = a.decode("ascii")
|
|
82
|
+
if isinstance(a, str):
|
|
83
|
+
if a and a != ".":
|
|
84
|
+
if sep in a:
|
|
85
|
+
for x in a.split(sep):
|
|
86
|
+
if x and x != ".":
|
|
87
|
+
append(x)
|
|
88
|
+
else:
|
|
89
|
+
append(a)
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
raise TypeError(
|
|
93
|
+
"argument must be str, int, bytes, os.PathLike, or SchemaPath; got %r"
|
|
94
|
+
% (type(a),)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return tuple(parts)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_dict(
|
|
101
|
+
cls: type[TSchemaPath],
|
|
102
|
+
data: Schema,
|
|
103
|
+
*args: Any,
|
|
104
|
+
separator: str = SPEC_SEPARATOR,
|
|
105
|
+
specification: Specification[Schema] = DRAFT202012,
|
|
106
|
+
base_uri: str = "",
|
|
107
|
+
handlers: ResolverHandlers = default_handlers,
|
|
108
|
+
spec_url: str | None = None,
|
|
109
|
+
ref_resolver_handlers: ResolverHandlers | None = None,
|
|
110
|
+
) -> TSchemaPath:
|
|
111
|
+
if spec_url is not None:
|
|
112
|
+
warnings.warn(
|
|
113
|
+
"spec_url parameter is deprecated. " "Use base_uri instead.",
|
|
114
|
+
DeprecationWarning,
|
|
115
|
+
)
|
|
116
|
+
base_uri = spec_url
|
|
117
|
+
if ref_resolver_handlers is not None:
|
|
118
|
+
warnings.warn(
|
|
119
|
+
"ref_resolver_handlers parameter is deprecated. "
|
|
120
|
+
"Use handlers instead.",
|
|
121
|
+
DeprecationWarning,
|
|
122
|
+
)
|
|
123
|
+
handlers = ref_resolver_handlers
|
|
124
|
+
|
|
125
|
+
accessor: SchemaAccessor = SchemaAccessor.from_schema(
|
|
126
|
+
data,
|
|
127
|
+
specification=specification,
|
|
128
|
+
base_uri=base_uri,
|
|
129
|
+
handlers=handlers,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return cls(accessor, *args, separator=separator)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_path(
|
|
136
|
+
cls: type[TSchemaPath],
|
|
137
|
+
path: Path,
|
|
138
|
+
) -> TSchemaPath:
|
|
139
|
+
reader = PathReader(path)
|
|
140
|
+
data, base_uri = reader.read()
|
|
141
|
+
return cls.from_dict(data, base_uri=base_uri)
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_file_path(
|
|
145
|
+
cls: type[TSchemaPath],
|
|
146
|
+
file_path: str,
|
|
147
|
+
) -> TSchemaPath:
|
|
148
|
+
reader = FilePathReader(file_path)
|
|
149
|
+
data, base_uri = reader.read()
|
|
150
|
+
return cls.from_dict(data, base_uri=base_uri)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def from_file(
|
|
154
|
+
cls: type[TSchemaPath],
|
|
155
|
+
fileobj: SupportsRead,
|
|
156
|
+
base_uri: str = "",
|
|
157
|
+
spec_url: str | None = None,
|
|
158
|
+
) -> TSchemaPath:
|
|
159
|
+
reader = FileReader(fileobj)
|
|
160
|
+
data, _ = reader.read()
|
|
161
|
+
return cls.from_dict(data, base_uri=base_uri, spec_url=spec_url)
|
|
162
|
+
|
|
163
|
+
def str_keys(self) -> Sequence[str]:
|
|
164
|
+
keys = list(self.keys())
|
|
165
|
+
if not is_str_sequence(keys):
|
|
166
|
+
raise TypeError(
|
|
167
|
+
f"Expected string keys, got {[type(x) for x in keys]}"
|
|
168
|
+
)
|
|
169
|
+
return keys
|
|
170
|
+
|
|
171
|
+
def str_items(self) -> Iterator[tuple[str, SchemaPath]]:
|
|
172
|
+
for key, value in self.items():
|
|
173
|
+
if not isinstance(key, str):
|
|
174
|
+
raise TypeError(f"Expected string keys, got {type(key)}")
|
|
175
|
+
yield key, value
|
|
176
|
+
|
|
177
|
+
@overload
|
|
178
|
+
def read_str(self) -> str: ...
|
|
179
|
+
|
|
180
|
+
@overload
|
|
181
|
+
def read_str(self, default: TDefault) -> str | TDefault: ...
|
|
182
|
+
|
|
183
|
+
def read_str(self, default: object = NOTSET) -> object:
|
|
184
|
+
try:
|
|
185
|
+
value = self.read_value()
|
|
186
|
+
except KeyError:
|
|
187
|
+
if default is not NOTSET:
|
|
188
|
+
return default
|
|
189
|
+
raise
|
|
190
|
+
if not isinstance(value, str):
|
|
191
|
+
raise TypeError(f"Expected a string value, got {type(value)}")
|
|
192
|
+
return value
|
|
193
|
+
|
|
194
|
+
@overload
|
|
195
|
+
def read_str_or_list(self) -> str | list[str]: ...
|
|
196
|
+
|
|
197
|
+
@overload
|
|
198
|
+
def read_str_or_list(
|
|
199
|
+
self, default: TDefault
|
|
200
|
+
) -> str | list[str] | TDefault: ...
|
|
201
|
+
|
|
202
|
+
def read_str_or_list(self, default: object = NOTSET) -> object:
|
|
203
|
+
try:
|
|
204
|
+
value = self.read_value()
|
|
205
|
+
except KeyError:
|
|
206
|
+
if default is not NOTSET:
|
|
207
|
+
return default
|
|
208
|
+
raise
|
|
209
|
+
if not isinstance(value, (str, list)):
|
|
210
|
+
raise TypeError(
|
|
211
|
+
f"Expected a string or a list of strings, got {type(value)}"
|
|
212
|
+
)
|
|
213
|
+
return value
|
|
214
|
+
|
|
215
|
+
@overload
|
|
216
|
+
def read_bool(self) -> bool: ...
|
|
217
|
+
|
|
218
|
+
@overload
|
|
219
|
+
def read_bool(self, default: TDefault) -> bool | TDefault: ...
|
|
220
|
+
|
|
221
|
+
def read_bool(self, default: object = NOTSET) -> object:
|
|
222
|
+
try:
|
|
223
|
+
value = self.read_value()
|
|
224
|
+
except KeyError:
|
|
225
|
+
if default is not NOTSET:
|
|
226
|
+
return default
|
|
227
|
+
raise
|
|
228
|
+
if not isinstance(value, bool):
|
|
229
|
+
if default is not NOTSET:
|
|
230
|
+
return default
|
|
231
|
+
raise TypeError(f"Expected a bool value, got {type(value)}")
|
|
232
|
+
return value
|
|
233
|
+
|
|
234
|
+
def as_uri(self) -> str:
|
|
235
|
+
return f"#/{str(self)}"
|
|
236
|
+
|
|
237
|
+
@contextmanager
|
|
238
|
+
def open(self) -> Any:
|
|
239
|
+
"""Open the path."""
|
|
240
|
+
# Cached path content
|
|
241
|
+
with self.resolve() as resolved:
|
|
242
|
+
yield resolved.contents
|
|
243
|
+
|
|
244
|
+
@contextmanager
|
|
245
|
+
def resolve(self) -> Iterator[Resolved[SchemaNode]]:
|
|
246
|
+
"""Resolve the path."""
|
|
247
|
+
# Cached path content
|
|
248
|
+
yield self._get_resolved
|
|
249
|
+
|
|
250
|
+
@cached_property
|
|
251
|
+
def _get_resolved(self) -> Resolved[SchemaNode]:
|
|
252
|
+
assert isinstance(self.accessor, SchemaAccessor)
|
|
253
|
+
with self.accessor.resolve(self.parts) as resolved:
|
|
254
|
+
return resolved
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
"""JSONSchema spec readers module."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
5
|
-
from typing import Hashable
|
|
6
|
-
from typing import Mapping
|
|
7
|
-
from typing import Tuple
|
|
8
4
|
|
|
9
5
|
from jsonschema_path.handlers import all_urls_handler
|
|
10
6
|
from jsonschema_path.handlers import file_handler
|
|
11
7
|
from jsonschema_path.handlers.protocols import SupportsRead
|
|
8
|
+
from jsonschema_path.typing import Schema
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
class BaseReader:
|
|
15
|
-
def read(self) ->
|
|
12
|
+
def read(self) -> tuple[Schema, str]:
|
|
16
13
|
raise NotImplementedError
|
|
17
14
|
|
|
18
15
|
|
|
@@ -20,7 +17,7 @@ class FileReader(BaseReader):
|
|
|
20
17
|
def __init__(self, fileobj: SupportsRead):
|
|
21
18
|
self.fileobj = fileobj
|
|
22
19
|
|
|
23
|
-
def read(self) ->
|
|
20
|
+
def read(self) -> tuple[Schema, str]:
|
|
24
21
|
return file_handler(self.fileobj), ""
|
|
25
22
|
|
|
26
23
|
|
|
@@ -28,7 +25,7 @@ class PathReader(BaseReader):
|
|
|
28
25
|
def __init__(self, path: Path):
|
|
29
26
|
self.path = path
|
|
30
27
|
|
|
31
|
-
def read(self) ->
|
|
28
|
+
def read(self) -> tuple[Schema, str]:
|
|
32
29
|
if not self.path.is_file():
|
|
33
30
|
raise OSError(f"No such file: {self.path}")
|
|
34
31
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import TypeGuard
|
|
5
|
+
|
|
6
|
+
from pathable.types import LookupKey as SchemaKey
|
|
7
|
+
from pathable.types import LookupNode as SchemaNode
|
|
8
|
+
from pathable.types import LookupValue as SchemaValue
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ResolverHandlers",
|
|
12
|
+
"Schema",
|
|
13
|
+
"SchemaNode",
|
|
14
|
+
"SchemaKey",
|
|
15
|
+
"SchemaValue",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
ResolverHandlers = Mapping[str, Any]
|
|
19
|
+
Schema = Mapping[str, Any]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_str_sequence(val: Sequence[object]) -> TypeGuard[Sequence[str]]:
|
|
23
|
+
"""Determines whether all objects in the list are strings"""
|
|
24
|
+
return all(isinstance(x, str) for x in val)
|
|
@@ -19,7 +19,7 @@ ignore_missing_imports = true
|
|
|
19
19
|
|
|
20
20
|
[tool.poetry]
|
|
21
21
|
name = "jsonschema-path"
|
|
22
|
-
version = "0.
|
|
22
|
+
version = "0.4.0"
|
|
23
23
|
description = "JSONSchema Spec with object-oriented paths"
|
|
24
24
|
authors = ["Artur Maciag <maciag.artur@gmail.com>"]
|
|
25
25
|
license = "Apache-2.0"
|
|
@@ -32,37 +32,40 @@ classifiers = [
|
|
|
32
32
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
33
|
"Operating System :: OS Independent",
|
|
34
34
|
"Programming Language :: Python :: 3",
|
|
35
|
-
"Programming Language :: Python :: 3.8",
|
|
36
|
-
"Programming Language :: Python :: 3.9",
|
|
37
35
|
"Programming Language :: Python :: 3.10",
|
|
38
36
|
"Programming Language :: Python :: 3.11",
|
|
39
37
|
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Programming Language :: Python :: 3.14",
|
|
40
40
|
"Topic :: Software Development :: Libraries",
|
|
41
41
|
]
|
|
42
42
|
|
|
43
43
|
[tool.poetry.dependencies]
|
|
44
|
-
pathable = "^0.
|
|
45
|
-
python = "
|
|
44
|
+
pathable = "^0.5.0"
|
|
45
|
+
python = ">=3.10,<4.0.0"
|
|
46
46
|
PyYAML = ">=5.1"
|
|
47
47
|
requests = {version = "^2.31.0", optional = true}
|
|
48
48
|
referencing = "<0.37.0"
|
|
49
49
|
|
|
50
|
-
[tool.poetry.dev
|
|
50
|
+
[tool.poetry.group.dev.dependencies]
|
|
51
|
+
tbump = "^6.11.0"
|
|
51
52
|
pre-commit = "*"
|
|
52
53
|
pytest = "^8.2.1"
|
|
53
|
-
pytest-flake8 = "=1.
|
|
54
|
-
pytest-cov = "
|
|
55
|
-
isort = "
|
|
56
|
-
black = "^
|
|
57
|
-
flynt = "1.0.
|
|
58
|
-
mypy = "^1.
|
|
54
|
+
pytest-flake8 = "=1.3.0"
|
|
55
|
+
pytest-cov = ">=5,<7"
|
|
56
|
+
isort = ">=5.13.2,<7.0.0"
|
|
57
|
+
black = "^25.1.0"
|
|
58
|
+
flynt = "1.0.6"
|
|
59
|
+
mypy = "^1.14.1"
|
|
59
60
|
types-PyYAML = "^6.0.12"
|
|
60
61
|
types-requests = "^2.31.0"
|
|
61
|
-
typing-extensions = "^4.10.0" # required by responses. See https://github.com/p1c2u/jsonschema-path/issues/44
|
|
62
62
|
responses = "^0.25.0"
|
|
63
|
-
deptry = "
|
|
64
|
-
pyflakes = "
|
|
65
|
-
|
|
63
|
+
deptry = ">=0.19.1,<0.24.0"
|
|
64
|
+
pyflakes = ">=2.5,<4.0"
|
|
65
|
+
ipdb = "^0.13.13"
|
|
66
|
+
|
|
67
|
+
[tool.poetry.extras]
|
|
68
|
+
requests = ["requests"]
|
|
66
69
|
|
|
67
70
|
[tool.pytest.ini_options]
|
|
68
71
|
addopts = """
|
|
@@ -82,3 +85,27 @@ line-length = 79
|
|
|
82
85
|
profile = "black"
|
|
83
86
|
line_length = 79
|
|
84
87
|
force_single_line = true
|
|
88
|
+
|
|
89
|
+
[tool.tbump]
|
|
90
|
+
|
|
91
|
+
[tool.tbump.git]
|
|
92
|
+
message_template = "Version {new_version}"
|
|
93
|
+
tag_template = "{new_version}"
|
|
94
|
+
|
|
95
|
+
[tool.tbump.version]
|
|
96
|
+
current = "0.4.0"
|
|
97
|
+
regex = '''
|
|
98
|
+
(?P<major>\d+)
|
|
99
|
+
\.
|
|
100
|
+
(?P<minor>\d+)
|
|
101
|
+
\.
|
|
102
|
+
(?P<patch>\d+)
|
|
103
|
+
(?P<prerelease>[a-z]+\d+)?
|
|
104
|
+
'''
|
|
105
|
+
|
|
106
|
+
[[tool.tbump.file]]
|
|
107
|
+
src = "jsonschema_path/__init__.py"
|
|
108
|
+
|
|
109
|
+
[[tool.tbump.file]]
|
|
110
|
+
src = "pyproject.toml"
|
|
111
|
+
search = 'version = "{current_version}"'
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
"""JSONSchema spec accessors module."""
|
|
2
|
-
|
|
3
|
-
from collections import deque
|
|
4
|
-
from contextlib import contextmanager
|
|
5
|
-
from typing import Any
|
|
6
|
-
from typing import Deque
|
|
7
|
-
from typing import Hashable
|
|
8
|
-
from typing import Iterator
|
|
9
|
-
from typing import List
|
|
10
|
-
from typing import Optional
|
|
11
|
-
from typing import Union
|
|
12
|
-
|
|
13
|
-
from pathable.accessors import LookupAccessor
|
|
14
|
-
from referencing import Registry
|
|
15
|
-
from referencing import Specification
|
|
16
|
-
from referencing._core import Resolved
|
|
17
|
-
from referencing._core import Resolver
|
|
18
|
-
from referencing.jsonschema import DRAFT202012
|
|
19
|
-
|
|
20
|
-
from jsonschema_path.handlers import default_handlers
|
|
21
|
-
from jsonschema_path.retrievers import SchemaRetriever
|
|
22
|
-
from jsonschema_path.typing import Lookup
|
|
23
|
-
from jsonschema_path.typing import ResolverHandlers
|
|
24
|
-
from jsonschema_path.typing import Schema
|
|
25
|
-
from jsonschema_path.utils import is_ref
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class ResolverAccessor(LookupAccessor):
|
|
29
|
-
def __init__(self, lookup: Lookup, resolver: Resolver[Lookup]):
|
|
30
|
-
super().__init__(lookup)
|
|
31
|
-
self.resolver = resolver
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class SchemaAccessor(ResolverAccessor):
|
|
35
|
-
@classmethod
|
|
36
|
-
def from_schema(
|
|
37
|
-
cls,
|
|
38
|
-
schema: Schema,
|
|
39
|
-
specification: Specification[Schema] = DRAFT202012,
|
|
40
|
-
base_uri: str = "",
|
|
41
|
-
handlers: ResolverHandlers = default_handlers,
|
|
42
|
-
) -> "SchemaAccessor":
|
|
43
|
-
retriever = SchemaRetriever(handlers, specification)
|
|
44
|
-
base_resource = specification.create_resource(schema)
|
|
45
|
-
registry: Registry[Schema] = Registry(
|
|
46
|
-
retrieve=retriever, # type: ignore
|
|
47
|
-
)
|
|
48
|
-
registry = registry.with_resource(base_uri, base_resource)
|
|
49
|
-
resolver = registry.resolver(base_uri=base_uri)
|
|
50
|
-
return cls(schema, resolver)
|
|
51
|
-
|
|
52
|
-
@contextmanager
|
|
53
|
-
def open(self, parts: List[Hashable]) -> Iterator[Union[Schema, Any]]:
|
|
54
|
-
parts_deque = deque(parts)
|
|
55
|
-
try:
|
|
56
|
-
resolved = self._resolve(self.lookup, parts_deque)
|
|
57
|
-
yield resolved.contents
|
|
58
|
-
finally:
|
|
59
|
-
pass
|
|
60
|
-
|
|
61
|
-
@contextmanager
|
|
62
|
-
def resolve(self, parts: List[Hashable]) -> Iterator[Resolved[Any]]:
|
|
63
|
-
parts_deque = deque(parts)
|
|
64
|
-
try:
|
|
65
|
-
yield self._resolve(self.lookup, parts_deque)
|
|
66
|
-
finally:
|
|
67
|
-
pass
|
|
68
|
-
|
|
69
|
-
def _resolve(
|
|
70
|
-
self,
|
|
71
|
-
contents: Schema,
|
|
72
|
-
parts_deque: Deque[Hashable],
|
|
73
|
-
resolver: Optional[Resolver[Schema]] = None,
|
|
74
|
-
) -> Resolved[Any]:
|
|
75
|
-
resolver = resolver or self.resolver
|
|
76
|
-
if is_ref(contents):
|
|
77
|
-
ref = contents["$ref"]
|
|
78
|
-
resolved = resolver.lookup(ref)
|
|
79
|
-
self.resolver = self.resolver._evolve(
|
|
80
|
-
self.resolver._base_uri,
|
|
81
|
-
registry=resolved.resolver._registry,
|
|
82
|
-
)
|
|
83
|
-
return self._resolve(
|
|
84
|
-
resolved.contents,
|
|
85
|
-
parts_deque,
|
|
86
|
-
resolver=resolved.resolver,
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
try:
|
|
90
|
-
part = parts_deque.popleft()
|
|
91
|
-
except IndexError:
|
|
92
|
-
return Resolved(contents=contents, resolver=resolver) # type: ignore
|
|
93
|
-
else:
|
|
94
|
-
target = contents[part]
|
|
95
|
-
return self._resolve(target, parts_deque, resolver=resolver)
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
"""JSONSchema spec paths module."""
|
|
2
|
-
|
|
3
|
-
import warnings
|
|
4
|
-
from contextlib import contextmanager
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
|
-
from typing import Iterator
|
|
8
|
-
from typing import Optional
|
|
9
|
-
from typing import Type
|
|
10
|
-
from typing import TypeVar
|
|
11
|
-
|
|
12
|
-
from pathable.paths import AccessorPath
|
|
13
|
-
from referencing import Specification
|
|
14
|
-
from referencing._core import Resolved
|
|
15
|
-
from referencing.jsonschema import DRAFT202012
|
|
16
|
-
|
|
17
|
-
from jsonschema_path.accessors import SchemaAccessor
|
|
18
|
-
from jsonschema_path.handlers import default_handlers
|
|
19
|
-
from jsonschema_path.handlers.protocols import SupportsRead
|
|
20
|
-
from jsonschema_path.readers import FilePathReader
|
|
21
|
-
from jsonschema_path.readers import FileReader
|
|
22
|
-
from jsonschema_path.readers import PathReader
|
|
23
|
-
from jsonschema_path.typing import ResolverHandlers
|
|
24
|
-
from jsonschema_path.typing import Schema
|
|
25
|
-
|
|
26
|
-
TSpec = TypeVar("TSpec", bound="SchemaPath")
|
|
27
|
-
|
|
28
|
-
SPEC_SEPARATOR = "#"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class SchemaPath(AccessorPath):
|
|
32
|
-
def __init__(self, accessor: SchemaAccessor, *args: Any, **kwargs: Any):
|
|
33
|
-
super().__init__(accessor, *args, **kwargs)
|
|
34
|
-
self._resolved_cached: Optional[Resolved[Any]] = None
|
|
35
|
-
|
|
36
|
-
@classmethod
|
|
37
|
-
def from_dict(
|
|
38
|
-
cls: Type[TSpec],
|
|
39
|
-
data: Schema,
|
|
40
|
-
*args: Any,
|
|
41
|
-
separator: str = SPEC_SEPARATOR,
|
|
42
|
-
specification: Specification[Schema] = DRAFT202012,
|
|
43
|
-
base_uri: str = "",
|
|
44
|
-
handlers: ResolverHandlers = default_handlers,
|
|
45
|
-
spec_url: Optional[str] = None,
|
|
46
|
-
ref_resolver_handlers: Optional[ResolverHandlers] = None,
|
|
47
|
-
) -> TSpec:
|
|
48
|
-
if spec_url is not None:
|
|
49
|
-
warnings.warn(
|
|
50
|
-
"spec_url parameter is deprecated. " "Use base_uri instead.",
|
|
51
|
-
DeprecationWarning,
|
|
52
|
-
)
|
|
53
|
-
base_uri = spec_url
|
|
54
|
-
if ref_resolver_handlers is not None:
|
|
55
|
-
warnings.warn(
|
|
56
|
-
"ref_resolver_handlers parameter is deprecated. "
|
|
57
|
-
"Use handlers instead.",
|
|
58
|
-
DeprecationWarning,
|
|
59
|
-
)
|
|
60
|
-
handlers = ref_resolver_handlers
|
|
61
|
-
|
|
62
|
-
accessor: SchemaAccessor = SchemaAccessor.from_schema(
|
|
63
|
-
data,
|
|
64
|
-
specification=specification,
|
|
65
|
-
base_uri=base_uri,
|
|
66
|
-
handlers=handlers,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return cls(accessor, *args, separator=separator)
|
|
70
|
-
|
|
71
|
-
@classmethod
|
|
72
|
-
def from_path(
|
|
73
|
-
cls: Type[TSpec],
|
|
74
|
-
path: Path,
|
|
75
|
-
) -> TSpec:
|
|
76
|
-
reader = PathReader(path)
|
|
77
|
-
data, base_uri = reader.read()
|
|
78
|
-
return cls.from_dict(data, base_uri=base_uri)
|
|
79
|
-
|
|
80
|
-
@classmethod
|
|
81
|
-
def from_file_path(
|
|
82
|
-
cls: Type[TSpec],
|
|
83
|
-
file_path: str,
|
|
84
|
-
) -> TSpec:
|
|
85
|
-
reader = FilePathReader(file_path)
|
|
86
|
-
data, base_uri = reader.read()
|
|
87
|
-
return cls.from_dict(data, base_uri=base_uri)
|
|
88
|
-
|
|
89
|
-
@classmethod
|
|
90
|
-
def from_file(
|
|
91
|
-
cls: Type[TSpec],
|
|
92
|
-
fileobj: SupportsRead,
|
|
93
|
-
base_uri: str = "",
|
|
94
|
-
spec_url: Optional[str] = None,
|
|
95
|
-
) -> TSpec:
|
|
96
|
-
reader = FileReader(fileobj)
|
|
97
|
-
data, _ = reader.read()
|
|
98
|
-
return cls.from_dict(data, base_uri=base_uri, spec_url=spec_url)
|
|
99
|
-
|
|
100
|
-
def contents(self) -> Any:
|
|
101
|
-
with self.open() as d:
|
|
102
|
-
return d
|
|
103
|
-
|
|
104
|
-
def exists(self) -> bool:
|
|
105
|
-
try:
|
|
106
|
-
self.contents()
|
|
107
|
-
except KeyError:
|
|
108
|
-
return False
|
|
109
|
-
else:
|
|
110
|
-
return True
|
|
111
|
-
|
|
112
|
-
def as_uri(self) -> str:
|
|
113
|
-
return f"#/{str(self)}"
|
|
114
|
-
|
|
115
|
-
@contextmanager
|
|
116
|
-
def open(self) -> Any:
|
|
117
|
-
"""Open the path."""
|
|
118
|
-
# Cached path content
|
|
119
|
-
with self.resolve() as resolved:
|
|
120
|
-
yield resolved.contents
|
|
121
|
-
|
|
122
|
-
@contextmanager
|
|
123
|
-
def resolve(self) -> Iterator[Resolved[Any]]:
|
|
124
|
-
"""Resolve the path."""
|
|
125
|
-
# Cached path content
|
|
126
|
-
if self._resolved_cached is None:
|
|
127
|
-
self._resolved_cached = self._get_resolved()
|
|
128
|
-
yield self._resolved_cached
|
|
129
|
-
|
|
130
|
-
def _get_resolved(self) -> Resolved[Any]:
|
|
131
|
-
assert isinstance(self.accessor, SchemaAccessor)
|
|
132
|
-
with self.accessor.resolve(self.parts) as resolved:
|
|
133
|
-
return resolved
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|