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.
Files changed (24) hide show
  1. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/PKG-INFO +46 -8
  2. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/README.rst +38 -1
  3. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/__init__.py +1 -1
  4. jsonschema_path-0.4.0/jsonschema_path/accessors.py +218 -0
  5. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/file.py +3 -5
  6. jsonschema_path-0.4.0/jsonschema_path/handlers/protocols.py +5 -0
  7. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/requests.py +2 -4
  8. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/urllib.py +1 -2
  9. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/loaders.py +3 -5
  10. jsonschema_path-0.4.0/jsonschema_path/paths.py +254 -0
  11. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/readers.py +4 -7
  12. jsonschema_path-0.4.0/jsonschema_path/typing.py +24 -0
  13. jsonschema_path-0.4.0/jsonschema_path/utils.py +9 -0
  14. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/pyproject.toml +43 -16
  15. jsonschema_path-0.3.4/jsonschema_path/accessors.py +0 -95
  16. jsonschema_path-0.3.4/jsonschema_path/handlers/protocols.py +0 -6
  17. jsonschema_path-0.3.4/jsonschema_path/paths.py +0 -133
  18. jsonschema_path-0.3.4/jsonschema_path/typing.py +0 -8
  19. jsonschema_path-0.3.4/jsonschema_path/utils.py +0 -8
  20. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/LICENSE +0 -0
  21. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/__init__.py +0 -0
  22. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/handlers/utils.py +0 -0
  23. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/py.typed +0 -0
  24. {jsonschema_path-0.3.4 → jsonschema_path-0.4.0}/jsonschema_path/retrievers.py +0 -0
@@ -1,29 +1,30 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: jsonschema-path
3
- Version: 0.3.4
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.8.0,<4.0.0
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.4.1,<0.5.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-2022, Artur Maciag, All rights reserved. Apache-2.0
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-2022, Artur Maciag, All rights reserved. Apache-2.0
143
+ Copyright (c) 2017-2025, Artur Maciag, All rights reserved. Apache-2.0
@@ -4,7 +4,7 @@ from jsonschema_path.paths import SchemaPath
4
4
 
5
5
  __author__ = "Artur Maciag"
6
6
  __email__ = "maciag.artur@gmail.com"
7
- __version__ = "0.3.4"
7
+ __version__ = "0.4.0"
8
8
  __url__ = "https://github.com/p1c2u/jsonschema-path"
9
9
  __license__ = "Apache-2.0"
10
10
 
@@ -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: Tuple[str, ...] = NotImplemented
33
+ allowed_schemes: tuple[str, ...] = NotImplemented
36
34
 
37
35
  def __init__(
38
- self, *allowed_schemes: str, file_handler: Optional[FileHandler] = None
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: Optional[FileHandler] = None,
61
+ file_handler: FileHandler | None = None,
64
62
  encoding: str = "utf-8",
65
63
  ):
66
64
  super().__init__(*allowed_schemes, file_handler=file_handler)
@@ -0,0 +1,5 @@
1
+ from typing import Protocol
2
+
3
+
4
+ class SupportsRead(Protocol):
5
+ def read(self, amount: int | None = 0) -> str: ...
@@ -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: Optional[FileHandler] = None,
20
+ file_handler: FileHandler | None = None,
23
21
  timeout: int = 10,
24
- verify: Optional[Union[bool, str]] = True,
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: Optional[FileHandler] = None,
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: Tuple[type, ...],
29
- namespace: Dict[str, Any],
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) -> Tuple[Mapping[Hashable, Any], str]:
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) -> Tuple[Mapping[Hashable, Any], str]:
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) -> Tuple[Mapping[Hashable, Any], str]:
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)
@@ -0,0 +1,9 @@
1
+ from typing import Any
2
+
3
+
4
+ def is_ref(item: Any) -> bool:
5
+ return (
6
+ isinstance(item, dict)
7
+ and "$ref" in item
8
+ and isinstance(item["$ref"], str)
9
+ )
@@ -19,7 +19,7 @@ ignore_missing_imports = true
19
19
 
20
20
  [tool.poetry]
21
21
  name = "jsonschema-path"
22
- version = "0.3.4"
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.4.1"
45
- python = "^3.8.0"
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-dependencies]
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.1.1"
54
- pytest-cov = "^5.0.0"
55
- isort = "^5.13.2"
56
- black = "^24.4.0"
57
- flynt = "1.0.1"
58
- mypy = "^1.9.0"
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 = "^0.19.1"
64
- pyflakes = "^2.5.0"
65
- bump2version = "^1.0.1"
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,6 +0,0 @@
1
- from typing import Optional
2
- from typing import Protocol
3
-
4
-
5
- class SupportsRead(Protocol):
6
- def read(self, amount: Optional[int] = 0) -> str: ...
@@ -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
@@ -1,8 +0,0 @@
1
- from typing import Any
2
- from typing import Hashable
3
- from typing import Mapping
4
-
5
- Lookup = Mapping[Hashable, Any]
6
-
7
- ResolverHandlers = Mapping[str, Any]
8
- Schema = Mapping[Hashable, Any]
@@ -1,8 +0,0 @@
1
- from typing import Any
2
- from typing import Hashable
3
- from typing import Mapping
4
- from typing import Optional
5
-
6
-
7
- def is_ref(item: Optional[Mapping[Hashable, Any]]) -> bool:
8
- return isinstance(item, dict) and "$ref" in item and item["$ref"].__hash__
File without changes