jsonschema-path 0.4.2__tar.gz → 0.4.4__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 (22) hide show
  1. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/PKG-INFO +18 -2
  2. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/README.rst +17 -1
  3. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/__init__.py +1 -1
  4. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/accessors.py +32 -51
  5. jsonschema_path-0.4.4/jsonschema_path/caches.py +108 -0
  6. jsonschema_path-0.4.4/jsonschema_path/nodes.py +29 -0
  7. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/paths.py +21 -3
  8. jsonschema_path-0.4.4/jsonschema_path/resolvers.py +91 -0
  9. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/pyproject.toml +2 -2
  10. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/LICENSE +0 -0
  11. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/handlers/__init__.py +0 -0
  12. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/handlers/file.py +0 -0
  13. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/handlers/protocols.py +0 -0
  14. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/handlers/requests.py +0 -0
  15. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/handlers/urllib.py +0 -0
  16. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/handlers/utils.py +0 -0
  17. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/loaders.py +0 -0
  18. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/py.typed +0 -0
  19. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/readers.py +0 -0
  20. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/retrievers.py +0 -0
  21. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/typing.py +0 -0
  22. {jsonschema_path-0.4.2 → jsonschema_path-0.4.4}/jsonschema_path/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonschema-path
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: JSONSchema Spec with object-oriented paths
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -120,6 +120,21 @@ Usage
120
120
  {'type': 'string', 'default': '1.0'}
121
121
 
122
122
 
123
+ Resolved cache
124
+ ##############
125
+
126
+ The resolved-path cache is intended for repeated path lookups and may significantly improve
127
+ ``read_value``/membership hot paths. Cache entries are invalidated when the
128
+ resolver registry evolves during reference resolution.
129
+
130
+ This cache is optional and disabled by default
131
+ (``resolved_cache_maxsize=0``). You can enable it when creating paths or
132
+ accessors, for example:
133
+
134
+ .. code-block:: python
135
+
136
+ >>> path = SchemaPath.from_dict(d, resolved_cache_maxsize=64)
137
+
123
138
  Benchmarks
124
139
  ##########
125
140
 
@@ -140,12 +155,13 @@ For a quick smoke run:
140
155
  poetry run python -m tests.benchmarks.bench_parse --output reports/bench-parse.quick.json --quick
141
156
  poetry run python -m tests.benchmarks.bench_lookup --output reports/bench-lookup.quick.json --quick
142
157
 
143
- You can also control repeats/warmup via env vars:
158
+ You can also control repeats/warmup and resolved cache maxsize via env vars:
144
159
 
145
160
  .. code-block:: console
146
161
 
147
162
  export JSONSCHEMA_PATH_BENCH_REPEATS=5
148
163
  export JSONSCHEMA_PATH_BENCH_WARMUP=1
164
+ export JSONSCHEMA_PATH_BENCH_RESOLVED_CACHE_MAXSIZE=64
149
165
 
150
166
  Compare two results:
151
167
 
@@ -90,6 +90,21 @@ Usage
90
90
  {'type': 'string', 'default': '1.0'}
91
91
 
92
92
 
93
+ Resolved cache
94
+ ##############
95
+
96
+ The resolved-path cache is intended for repeated path lookups and may significantly improve
97
+ ``read_value``/membership hot paths. Cache entries are invalidated when the
98
+ resolver registry evolves during reference resolution.
99
+
100
+ This cache is optional and disabled by default
101
+ (``resolved_cache_maxsize=0``). You can enable it when creating paths or
102
+ accessors, for example:
103
+
104
+ .. code-block:: python
105
+
106
+ >>> path = SchemaPath.from_dict(d, resolved_cache_maxsize=64)
107
+
93
108
  Benchmarks
94
109
  ##########
95
110
 
@@ -110,12 +125,13 @@ For a quick smoke run:
110
125
  poetry run python -m tests.benchmarks.bench_parse --output reports/bench-parse.quick.json --quick
111
126
  poetry run python -m tests.benchmarks.bench_lookup --output reports/bench-lookup.quick.json --quick
112
127
 
113
- You can also control repeats/warmup via env vars:
128
+ You can also control repeats/warmup and resolved cache maxsize via env vars:
114
129
 
115
130
  .. code-block:: console
116
131
 
117
132
  export JSONSCHEMA_PATH_BENCH_REPEATS=5
118
133
  export JSONSCHEMA_PATH_BENCH_WARMUP=1
134
+ export JSONSCHEMA_PATH_BENCH_RESOLVED_CACHE_MAXSIZE=64
119
135
 
120
136
  Compare two results:
121
137
 
@@ -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.4.2"
7
+ __version__ = "0.4.4"
8
8
  __url__ = "https://github.com/p1c2u/jsonschema-path"
9
9
  __license__ = "Apache-2.0"
10
10
 
@@ -17,21 +17,32 @@ from referencing._core import Resolved
17
17
  from referencing._core import Resolver
18
18
  from referencing.jsonschema import DRAFT202012
19
19
 
20
+ from jsonschema_path.caches import FullPathResolvedCache
20
21
  from jsonschema_path.handlers import default_handlers
22
+ from jsonschema_path.resolvers import CachedPathResolver
21
23
  from jsonschema_path.retrievers import SchemaRetriever
22
24
  from jsonschema_path.typing import ResolverHandlers
23
25
  from jsonschema_path.typing import Schema
24
- from jsonschema_path.utils import is_ref
25
26
 
26
27
 
27
28
  class SchemaAccessor(LookupAccessor):
28
- _resolver_refs: dict[int, Resolver[Schema] | None] = {}
29
+ def __init__(
30
+ self,
31
+ schema: Schema,
32
+ resolver: Resolver[Schema],
33
+ resolved_cache_maxsize: int = 0,
34
+ ):
35
+ if resolved_cache_maxsize < 0:
36
+ raise ValueError("resolved_cache_maxsize must be >= 0")
29
37
 
30
- def __init__(self, schema: Schema, resolver: Resolver[Schema]):
31
38
  super().__init__(cast(LookupNode, schema))
32
- self.resolver = resolver
33
-
34
- self._resolver_refs[id(schema)] = resolver
39
+ self._path_resolver: CachedPathResolver = CachedPathResolver(
40
+ resolver,
41
+ )
42
+ self._resolved_cache_maxsize = resolved_cache_maxsize
43
+ self._resolved_cache: FullPathResolvedCache = FullPathResolvedCache(
44
+ maxsize=resolved_cache_maxsize
45
+ )
35
46
 
36
47
  @classmethod
37
48
  def from_schema(
@@ -40,6 +51,7 @@ class SchemaAccessor(LookupAccessor):
40
51
  specification: Specification[Schema] = DRAFT202012,
41
52
  base_uri: str = "",
42
53
  handlers: ResolverHandlers | None = None,
54
+ resolved_cache_maxsize: int = 0,
43
55
  ) -> "SchemaAccessor":
44
56
  if handlers is None:
45
57
  handlers = default_handlers
@@ -50,7 +62,11 @@ class SchemaAccessor(LookupAccessor):
50
62
  )
51
63
  registry = registry.with_resource(base_uri, base_resource)
52
64
  resolver = registry.resolver(base_uri=base_uri)
53
- return cls(schema, resolver)
65
+ return cls(
66
+ schema,
67
+ resolver,
68
+ resolved_cache_maxsize=resolved_cache_maxsize,
69
+ )
54
70
 
55
71
  def __getitem__(self, parts: Sequence[LookupKey]) -> LookupNode:
56
72
  resolved = self.get_resolved(parts)
@@ -143,49 +159,14 @@ class SchemaAccessor(LookupAccessor):
143
159
  pass
144
160
 
145
161
  def get_resolved(self, parts: Sequence[LookupKey]) -> Resolved[LookupNode]:
146
- resolved = self._get_resolved(self.node, parts, resolver=self.resolver)
147
- self.resolver = self.resolver._evolve(
148
- self.resolver._base_uri,
149
- registry=resolved.resolver._registry,
150
- )
151
- return resolved
162
+ cached_resolved = self._resolved_cache.get(parts)
163
+ if cached_resolved is not None:
164
+ return cached_resolved
152
165
 
153
- @classmethod
154
- def _get_resolved(
155
- cls,
156
- node: LookupNode,
157
- parts: Sequence[LookupKey],
158
- resolver: Resolver[Schema] | None = None,
159
- ) -> Resolved[LookupNode]:
160
- if resolver is None:
161
- raise ValueError("resolver must be provided")
162
-
163
- current_node: LookupNode = node
164
- current_resolver: Resolver[Schema] = resolver
165
-
166
- for part in parts:
167
- resolved = cls._resolve_node(current_node, current_resolver)
168
- current_node, current_resolver = (
169
- resolved.contents,
170
- resolved.resolver,
171
- )
172
- current_node = cls._get_subnode(current_node, part)
173
-
174
- resolved = cls._resolve_node(current_node, current_resolver)
175
- return cast(Resolved[LookupNode], resolved)
166
+ result = self._path_resolver.resolve(self.node, parts)
167
+ if result.registry_changed:
168
+ self._resolved_cache.invalidate()
176
169
 
177
- @classmethod
178
- def _resolve_node(
179
- cls,
180
- node: LookupNode,
181
- resolver: Resolver[Schema],
182
- ) -> Resolved[Schema]:
183
- if is_ref(node):
184
- ref_node = cls._get_subnode(node, "$ref")
185
- ref = cls._read_node(ref_node)
186
- resolved = resolver.lookup(ref)
187
- return cls._resolve_node(
188
- resolved.contents,
189
- resolved.resolver,
190
- )
191
- return Resolved(cast(Schema, node), resolver) # type: ignore
170
+ self._resolved_cache.set(parts, result.resolved)
171
+
172
+ return result.resolved
@@ -0,0 +1,108 @@
1
+ """JSONSchema path caches module."""
2
+
3
+ from collections import OrderedDict
4
+ from collections.abc import Sequence
5
+
6
+ from pathable.types import LookupKey
7
+ from pathable.types import LookupNode
8
+ from referencing._core import Resolved
9
+
10
+
11
+ class FullPathResolvedCache:
12
+ def __init__(self, maxsize: int):
13
+ self._maxsize = maxsize
14
+ self._generation = 0
15
+ self._cache: OrderedDict[
16
+ tuple[tuple[LookupKey, ...], int],
17
+ Resolved[LookupNode],
18
+ ] = OrderedDict()
19
+
20
+ def _make_key(
21
+ self,
22
+ parts: Sequence[LookupKey],
23
+ ) -> tuple[tuple[LookupKey, ...], int] | None:
24
+ if self._maxsize <= 0:
25
+ return None
26
+
27
+ parts_tuple = tuple(parts)
28
+ try:
29
+ hash(parts_tuple)
30
+ except TypeError:
31
+ return None
32
+
33
+ return (parts_tuple, self._generation)
34
+
35
+ def get(
36
+ self,
37
+ parts: Sequence[LookupKey],
38
+ ) -> Resolved[LookupNode] | None:
39
+ key = self._make_key(parts)
40
+ if key is None:
41
+ return None
42
+
43
+ cached = self._cache.get(key)
44
+ if cached is None:
45
+ return None
46
+
47
+ self._cache.move_to_end(key)
48
+ return cached
49
+
50
+ def set(
51
+ self,
52
+ parts: Sequence[LookupKey],
53
+ resolved: Resolved[LookupNode],
54
+ ) -> None:
55
+ key = self._make_key(parts)
56
+ if key is None:
57
+ return
58
+
59
+ self._cache[key] = resolved
60
+ self._cache.move_to_end(key)
61
+ if len(self._cache) > self._maxsize:
62
+ self._cache.popitem(last=False)
63
+
64
+ def invalidate(self) -> None:
65
+ self._generation += 1
66
+ self._cache.clear()
67
+
68
+
69
+ class PrefixResolvedCache:
70
+ def __init__(self) -> None:
71
+ self._cache: dict[tuple[LookupKey, ...], Resolved[LookupNode]] = {}
72
+
73
+ def seed_root(self, resolved: Resolved[LookupNode]) -> None:
74
+ self._cache[()] = resolved
75
+
76
+ def longest_prefix_hit(
77
+ self,
78
+ parts: tuple[LookupKey, ...],
79
+ ) -> tuple[int, Resolved[LookupNode]] | None:
80
+ for idx in range(len(parts) - 1, -1, -1):
81
+ prefix = parts[:idx]
82
+ try:
83
+ cached = self._cache.get(prefix)
84
+ except TypeError:
85
+ continue
86
+
87
+ if cached is not None:
88
+ return idx, cached
89
+
90
+ return None
91
+
92
+ def store_intermediate(
93
+ self,
94
+ parts: tuple[LookupKey, ...],
95
+ index: int,
96
+ resolved: Resolved[LookupNode],
97
+ ) -> None:
98
+ if index >= len(parts) - 1:
99
+ return
100
+
101
+ prefix = parts[: index + 1]
102
+ try:
103
+ self._cache[prefix] = resolved
104
+ except TypeError:
105
+ pass
106
+
107
+ def invalidate(self) -> None:
108
+ self._cache.clear()
@@ -0,0 +1,29 @@
1
+ """JSONSchema spec nodes module."""
2
+
3
+ from typing import cast
4
+
5
+ from pathable.accessors import LookupAccessor
6
+ from pathable.types import LookupNode
7
+ from referencing._core import Resolved
8
+ from referencing._core import Resolver
9
+
10
+ from jsonschema_path.typing import Schema
11
+ from jsonschema_path.utils import is_ref
12
+
13
+
14
+ class SchemaNode(LookupAccessor):
15
+ @classmethod
16
+ def _resolve_node(
17
+ cls,
18
+ node: LookupNode,
19
+ resolver: Resolver[Schema],
20
+ ) -> Resolved[Schema]:
21
+ if is_ref(node):
22
+ ref_node = cls._get_subnode(node, "$ref")
23
+ ref = cls._read_node(ref_node)
24
+ resolved = resolver.lookup(ref)
25
+ return cls._resolve_node(
26
+ resolved.contents,
27
+ resolved.resolver,
28
+ )
29
+ return Resolved(cast(Schema, node), resolver) # type: ignore
@@ -105,6 +105,7 @@ class SchemaPath(AccessorPath[SchemaNode, SchemaKey, SchemaValue]):
105
105
  specification: Specification[Schema] = DRAFT202012,
106
106
  base_uri: str = "",
107
107
  handlers: ResolverHandlers = default_handlers,
108
+ resolved_cache_maxsize: int = 0,
108
109
  spec_url: str | None = None,
109
110
  ref_resolver_handlers: ResolverHandlers | None = None,
110
111
  ) -> TSchemaPath:
@@ -127,6 +128,7 @@ class SchemaPath(AccessorPath[SchemaNode, SchemaKey, SchemaValue]):
127
128
  specification=specification,
128
129
  base_uri=base_uri,
129
130
  handlers=handlers,
131
+ resolved_cache_maxsize=resolved_cache_maxsize,
130
132
  )
131
133
 
132
134
  return cls(accessor, *args, separator=separator)
@@ -135,19 +137,29 @@ class SchemaPath(AccessorPath[SchemaNode, SchemaKey, SchemaValue]):
135
137
  def from_path(
136
138
  cls: type[TSchemaPath],
137
139
  path: Path,
140
+ resolved_cache_maxsize: int = 0,
138
141
  ) -> TSchemaPath:
139
142
  reader = PathReader(path)
140
143
  data, base_uri = reader.read()
141
- return cls.from_dict(data, base_uri=base_uri)
144
+ return cls.from_dict(
145
+ data,
146
+ base_uri=base_uri,
147
+ resolved_cache_maxsize=resolved_cache_maxsize,
148
+ )
142
149
 
143
150
  @classmethod
144
151
  def from_file_path(
145
152
  cls: type[TSchemaPath],
146
153
  file_path: str,
154
+ resolved_cache_maxsize: int = 0,
147
155
  ) -> TSchemaPath:
148
156
  reader = FilePathReader(file_path)
149
157
  data, base_uri = reader.read()
150
- return cls.from_dict(data, base_uri=base_uri)
158
+ return cls.from_dict(
159
+ data,
160
+ base_uri=base_uri,
161
+ resolved_cache_maxsize=resolved_cache_maxsize,
162
+ )
151
163
 
152
164
  @classmethod
153
165
  def from_file(
@@ -155,10 +167,16 @@ class SchemaPath(AccessorPath[SchemaNode, SchemaKey, SchemaValue]):
155
167
  fileobj: SupportsRead,
156
168
  base_uri: str = "",
157
169
  spec_url: str | None = None,
170
+ resolved_cache_maxsize: int = 0,
158
171
  ) -> TSchemaPath:
159
172
  reader = FileReader(fileobj)
160
173
  data, _ = reader.read()
161
- return cls.from_dict(data, base_uri=base_uri, spec_url=spec_url)
174
+ return cls.from_dict(
175
+ data,
176
+ base_uri=base_uri,
177
+ spec_url=spec_url,
178
+ resolved_cache_maxsize=resolved_cache_maxsize,
179
+ )
162
180
 
163
181
  def str_keys(self) -> Sequence[str]:
164
182
  keys = list(self.keys())
@@ -0,0 +1,91 @@
1
+ from collections.abc import Sequence
2
+ from dataclasses import dataclass
3
+ from typing import cast
4
+
5
+ from pathable.types import LookupKey
6
+ from pathable.types import LookupNode
7
+ from referencing import Registry
8
+ from referencing._core import Resolved
9
+ from referencing._core import Resolver
10
+
11
+ from jsonschema_path.caches import PrefixResolvedCache
12
+ from jsonschema_path.nodes import SchemaNode
13
+ from jsonschema_path.typing import Schema
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ResolveResult:
18
+ resolved: Resolved[LookupNode]
19
+ registry_changed: bool
20
+
21
+
22
+ class CachedPathResolver:
23
+ def __init__(self, resolver: Resolver[Schema]):
24
+ self.resolver = resolver
25
+ self.prefix_cache = PrefixResolvedCache()
26
+
27
+ def resolve(
28
+ self,
29
+ node: LookupNode,
30
+ parts: Sequence[LookupKey],
31
+ ) -> ResolveResult:
32
+ resolved = self._resolve_with_prefix_cache(node, parts)
33
+ registry_changed = self._sync_registry(resolved.resolver._registry)
34
+ return ResolveResult(
35
+ resolved=resolved,
36
+ registry_changed=registry_changed,
37
+ )
38
+
39
+ def _resolve_with_prefix_cache(
40
+ self,
41
+ node: LookupNode,
42
+ parts: Sequence[LookupKey],
43
+ ) -> Resolved[LookupNode]:
44
+
45
+ parts_tuple = tuple(parts)
46
+ cached_prefix = self.prefix_cache.longest_prefix_hit(parts_tuple)
47
+ if cached_prefix is None:
48
+ root_resolved_schema = SchemaNode._resolve_node(
49
+ node,
50
+ self.resolver,
51
+ )
52
+ resolved = cast(Resolved[LookupNode], root_resolved_schema)
53
+ current_node = cast(LookupNode, root_resolved_schema.contents)
54
+ current_resolver: Resolver[Schema] = root_resolved_schema.resolver
55
+ start = 0
56
+ self.prefix_cache.seed_root(resolved)
57
+ else:
58
+ start, resolved = cached_prefix
59
+ current_node = resolved.contents
60
+ current_resolver = cast(Resolver[Schema], resolved.resolver)
61
+
62
+ for index in range(start, len(parts_tuple)):
63
+ part = parts_tuple[index]
64
+ current_node = SchemaNode._get_subnode(current_node, part)
65
+ resolved_schema = SchemaNode._resolve_node(
66
+ current_node,
67
+ current_resolver,
68
+ )
69
+ resolved = cast(Resolved[LookupNode], resolved_schema)
70
+ current_node, current_resolver = (
71
+ resolved.contents,
72
+ resolved_schema.resolver,
73
+ )
74
+ self.prefix_cache.store_intermediate(
75
+ parts_tuple,
76
+ index,
77
+ resolved,
78
+ )
79
+
80
+ return resolved
81
+
82
+ def _sync_registry(self, registry: Registry[LookupNode]) -> bool:
83
+ if registry is self.resolver._registry:
84
+ return False
85
+
86
+ self.resolver = self.resolver._evolve(
87
+ self.resolver._base_uri,
88
+ registry=registry,
89
+ )
90
+ self.prefix_cache.invalidate()
91
+ return True
@@ -19,7 +19,7 @@ ignore_missing_imports = true
19
19
 
20
20
  [tool.poetry]
21
21
  name = "jsonschema-path"
22
- version = "0.4.2"
22
+ version = "0.4.4"
23
23
  description = "JSONSchema Spec with object-oriented paths"
24
24
  authors = ["Artur Maciag <maciag.artur@gmail.com>"]
25
25
  license = "Apache-2.0"
@@ -93,7 +93,7 @@ message_template = "Version {new_version}"
93
93
  tag_template = "{new_version}"
94
94
 
95
95
  [tool.tbump.version]
96
- current = "0.4.2"
96
+ current = "0.4.4"
97
97
  regex = '''
98
98
  (?P<major>\d+)
99
99
  \.
File without changes