jsonschema-path 0.4.2__tar.gz → 0.4.3__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 (19) hide show
  1. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/PKG-INFO +18 -2
  2. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/README.rst +17 -1
  3. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/__init__.py +1 -1
  4. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/accessors.py +59 -6
  5. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/paths.py +21 -3
  6. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/pyproject.toml +2 -2
  7. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/LICENSE +0 -0
  8. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/handlers/__init__.py +0 -0
  9. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/handlers/file.py +0 -0
  10. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/handlers/protocols.py +0 -0
  11. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/handlers/requests.py +0 -0
  12. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/handlers/urllib.py +0 -0
  13. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/handlers/utils.py +0 -0
  14. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/loaders.py +0 -0
  15. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/py.typed +0 -0
  16. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/readers.py +0 -0
  17. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/retrievers.py +0 -0
  18. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/jsonschema_path/typing.py +0 -0
  19. {jsonschema_path-0.4.2 → jsonschema_path-0.4.3}/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.3
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.3"
8
8
  __url__ = "https://github.com/p1c2u/jsonschema-path"
9
9
  __license__ = "Apache-2.0"
10
10
 
@@ -1,5 +1,6 @@
1
1
  """JSONSchema spec accessors module."""
2
2
 
3
+ from collections import OrderedDict
3
4
  from collections.abc import Hashable
4
5
  from collections.abc import Iterator
5
6
  from collections.abc import Sequence
@@ -27,9 +28,23 @@ from jsonschema_path.utils import is_ref
27
28
  class SchemaAccessor(LookupAccessor):
28
29
  _resolver_refs: dict[int, Resolver[Schema] | None] = {}
29
30
 
30
- def __init__(self, schema: Schema, resolver: Resolver[Schema]):
31
+ def __init__(
32
+ self,
33
+ schema: Schema,
34
+ resolver: Resolver[Schema],
35
+ resolved_cache_maxsize: int = 0,
36
+ ):
37
+ if resolved_cache_maxsize < 0:
38
+ raise ValueError("resolved_cache_maxsize must be >= 0")
39
+
31
40
  super().__init__(cast(LookupNode, schema))
32
41
  self.resolver = resolver
42
+ self._resolved_cache_maxsize = resolved_cache_maxsize
43
+ self._resolved_cache: OrderedDict[
44
+ tuple[tuple[LookupKey, ...], int],
45
+ Resolved[LookupNode],
46
+ ] = OrderedDict()
47
+ self._resolver_version = 0
33
48
 
34
49
  self._resolver_refs[id(schema)] = resolver
35
50
 
@@ -40,6 +55,7 @@ class SchemaAccessor(LookupAccessor):
40
55
  specification: Specification[Schema] = DRAFT202012,
41
56
  base_uri: str = "",
42
57
  handlers: ResolverHandlers | None = None,
58
+ resolved_cache_maxsize: int = 0,
43
59
  ) -> "SchemaAccessor":
44
60
  if handlers is None:
45
61
  handlers = default_handlers
@@ -50,7 +66,11 @@ class SchemaAccessor(LookupAccessor):
50
66
  )
51
67
  registry = registry.with_resource(base_uri, base_resource)
52
68
  resolver = registry.resolver(base_uri=base_uri)
53
- return cls(schema, resolver)
69
+ return cls(
70
+ schema,
71
+ resolver,
72
+ resolved_cache_maxsize=resolved_cache_maxsize,
73
+ )
54
74
 
55
75
  def __getitem__(self, parts: Sequence[LookupKey]) -> LookupNode:
56
76
  resolved = self.get_resolved(parts)
@@ -143,13 +163,46 @@ class SchemaAccessor(LookupAccessor):
143
163
  pass
144
164
 
145
165
  def get_resolved(self, parts: Sequence[LookupKey]) -> Resolved[LookupNode]:
166
+ cache_key = self._resolved_cache_key(parts)
167
+ if cache_key is not None:
168
+ cached_resolved = self._resolved_cache.get(cache_key)
169
+ if cached_resolved is not None:
170
+ self._resolved_cache.move_to_end(cache_key)
171
+ return cached_resolved
172
+
146
173
  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
- )
174
+ if resolved.resolver._registry is not self.resolver._registry:
175
+ self.resolver = self.resolver._evolve(
176
+ self.resolver._base_uri,
177
+ registry=resolved.resolver._registry,
178
+ )
179
+ self._resolver_version += 1
180
+ self._resolved_cache.clear()
181
+
182
+ cache_key = self._resolved_cache_key(parts)
183
+ if cache_key is not None:
184
+ self._resolved_cache[cache_key] = resolved
185
+ self._resolved_cache.move_to_end(cache_key)
186
+ if len(self._resolved_cache) > self._resolved_cache_maxsize:
187
+ self._resolved_cache.popitem(last=False)
188
+
151
189
  return resolved
152
190
 
191
+ def _resolved_cache_key(
192
+ self,
193
+ parts: Sequence[LookupKey],
194
+ ) -> tuple[tuple[LookupKey, ...], int] | None:
195
+ if self._resolved_cache_maxsize <= 0:
196
+ return None
197
+
198
+ parts_tuple = tuple(parts)
199
+ try:
200
+ hash(parts_tuple)
201
+ except TypeError:
202
+ return None
203
+
204
+ return (parts_tuple, self._resolver_version)
205
+
153
206
  @classmethod
154
207
  def _get_resolved(
155
208
  cls,
@@ -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())
@@ -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.3"
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.3"
97
97
  regex = '''
98
98
  (?P<major>\d+)
99
99
  \.
File without changes