jentic-openapi-traverse 1.0.0a29__py3-none-any.whl

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.
@@ -0,0 +1,14 @@
1
+ """Low-level OpenAPI datamodel traversal."""
2
+
3
+ from .merge import merge_visitors
4
+ from .path import NodePath
5
+ from .traversal import BREAK, DataModelLowVisitor, traverse
6
+
7
+
8
+ __all__ = [
9
+ "DataModelLowVisitor",
10
+ "traverse",
11
+ "BREAK",
12
+ "NodePath",
13
+ "merge_visitors",
14
+ ]
@@ -0,0 +1,138 @@
1
+ """Utilities for introspecting datamodel fields."""
2
+
3
+ from dataclasses import is_dataclass
4
+ from typing import Any
5
+
6
+ from jentic.apitools.openapi.datamodels.low.fields import fixed_fields, patterned_fields
7
+ from jentic.apitools.openapi.datamodels.low.sources import (
8
+ FieldSource,
9
+ KeySource,
10
+ ValueSource,
11
+ )
12
+
13
+
14
+ __all__ = [
15
+ "get_traversable_fields",
16
+ "unwrap_value",
17
+ "is_datamodel_node",
18
+ "get_yaml_field_name",
19
+ ]
20
+
21
+
22
+ def get_yaml_field_name(node_class: type, python_field_name: str) -> str:
23
+ """
24
+ Convert Python field name to YAML field name using metadata.
25
+
26
+ Args:
27
+ node_class: Datamodel class type
28
+ python_field_name: Python attribute name (e.g., "external_docs")
29
+
30
+ Returns:
31
+ YAML field name from metadata, or python_field_name if not found
32
+ (e.g., "externalDocs")
33
+ """
34
+ fixed = fixed_fields(node_class)
35
+ patterned = patterned_fields(node_class)
36
+
37
+ # Try fixed fields first, then patterned
38
+ field_obj = fixed.get(python_field_name) or patterned.get(python_field_name)
39
+ if field_obj:
40
+ return field_obj.metadata.get("yaml_name", python_field_name)
41
+
42
+ return python_field_name
43
+
44
+
45
+ # Cache of field names to check per class type
46
+ # {Info: ["title", "description", "contact", ...], Operation: [...], ...}
47
+ _FIELD_NAMES_CACHE: dict[type, list[str]] = {}
48
+
49
+
50
+ def get_traversable_fields(node):
51
+ """
52
+ Get all fields that should be traversed in a datamodel node.
53
+
54
+ Uses field metadata (fixed_field, patterned_field) to identify OpenAPI
55
+ specification fields. This leverages the explicit field marking system
56
+ from the datamodels package.
57
+
58
+ Caches field names per class type for performance.
59
+
60
+ Args:
61
+ node: Datamodel node (dataclass instance)
62
+
63
+ Returns:
64
+ List of (field_name, field_value) tuples
65
+ """
66
+ if not is_dataclass(node):
67
+ return []
68
+
69
+ node_class = type(node)
70
+
71
+ # Get or compute field names for this class
72
+ if node_class not in _FIELD_NAMES_CACHE:
73
+ # Get all OpenAPI spec fields (fixed + patterned)
74
+ fixed = fixed_fields(node_class)
75
+ patterned = patterned_fields(node_class)
76
+ # Combine and extract field names
77
+ field_names = list(fixed.keys()) + list(patterned.keys())
78
+ _FIELD_NAMES_CACHE[node_class] = field_names
79
+
80
+ # Use cached field names
81
+ result = []
82
+ for field_name in _FIELD_NAMES_CACHE[node_class]:
83
+ value = getattr(node, field_name, None)
84
+
85
+ # Skip None values
86
+ if value is None:
87
+ continue
88
+
89
+ # Check if it's traversable
90
+ unwrapped = unwrap_value(value)
91
+
92
+ # Skip scalar primitives
93
+ if isinstance(unwrapped, (str, int, float, bool, type(None))):
94
+ continue
95
+
96
+ result.append((field_name, value))
97
+
98
+ return result
99
+
100
+
101
+ def unwrap_value(value: Any) -> Any:
102
+ """
103
+ Unwrap FieldSource/ValueSource/KeySource to get actual value.
104
+
105
+ Wrapper types (FieldSource, ValueSource, KeySource) are used to preserve
106
+ source location information for field values. This function extracts the
107
+ actual value from these wrappers.
108
+
109
+ This excludes datamodel nodes like Example which may have .value field
110
+ but are not wrapper types.
111
+
112
+ Args:
113
+ value: Potentially wrapped value
114
+
115
+ Returns:
116
+ Unwrapped value, or original if not wrapped
117
+ """
118
+ # Check if it's a wrapper type
119
+ if isinstance(value, (FieldSource, ValueSource, KeySource)):
120
+ return value.value
121
+ return value
122
+
123
+
124
+ def is_datamodel_node(value):
125
+ """
126
+ Check if value is a low-level datamodel object.
127
+
128
+ Low-level datamodels are distinguished by having a root_node field,
129
+ which contains the YAML source location information. This excludes
130
+ wrapper types (FieldSource, ValueSource, KeySource) and other dataclasses.
131
+
132
+ Args:
133
+ value: Value to check
134
+
135
+ Returns:
136
+ True if it's a low-level datamodel node, False otherwise
137
+ """
138
+ return is_dataclass(value) and hasattr(value, "root_node")
@@ -0,0 +1,111 @@
1
+ """Visitor merging utilities (ApiDOM pattern)."""
2
+
3
+ from .path import NodePath
4
+ from .traversal import BREAK
5
+
6
+
7
+ __all__ = ["merge_visitors"]
8
+
9
+
10
+ def merge_visitors(*visitors) -> object:
11
+ """
12
+ Merge multiple visitors into one composite visitor (ApiDOM semantics).
13
+
14
+ Each visitor maintains independent state:
15
+ - If visitor[i] returns False, only that visitor skips children (resumes when leaving)
16
+ - If visitor[i] returns BREAK, only that visitor stops permanently
17
+ - Other visitors continue normally
18
+
19
+ This matches ApiDOM's per-visitor control model where each visitor can
20
+ independently skip subtrees or stop without affecting other visitors.
21
+
22
+ Args:
23
+ *visitors: Visitor objects (with visit_* methods)
24
+
25
+ Returns:
26
+ A new visitor object that runs all visitors with independent state
27
+
28
+ Example:
29
+ security_check = SecurityCheckVisitor()
30
+ counter = OperationCounterVisitor()
31
+ validator = SchemaValidatorVisitor()
32
+
33
+ # Each visitor can skip/break independently
34
+ merged = merge_visitors(security_check, counter, validator)
35
+ traverse(doc, merged)
36
+
37
+ # If security_check.visit_PathItem returns False:
38
+ # - security_check skips PathItem's children
39
+ # - counter and validator still visit children normally
40
+ """
41
+
42
+ class MergedVisitor:
43
+ """Composite visitor with per-visitor state tracking (ApiDOM pattern)."""
44
+
45
+ def __init__(self, visitors):
46
+ self.visitors = visitors
47
+ # State per visitor: None = active, NodePath = skipping, BREAK = stopped
48
+ self._skipping_state: list[NodePath | object | None] = [None] * len(visitors)
49
+
50
+ def _is_active(self, visitor_idx):
51
+ """Check if visitor is active (not skipping or stopped)."""
52
+ return self._skipping_state[visitor_idx] is None
53
+
54
+ def _is_skipping_node(self, visitor_idx, path):
55
+ """Check if we're leaving the exact node this visitor skipped."""
56
+ state = self._skipping_state[visitor_idx]
57
+ if state is None or state is BREAK:
58
+ return False
59
+ # At this point, state must be a NodePath
60
+ # Compare node identity (not equality)
61
+ assert isinstance(state, NodePath)
62
+ return state.node is path.node
63
+
64
+ def __getattr__(self, name):
65
+ """
66
+ Dynamically handle visit_* method calls.
67
+
68
+ Maintains per-visitor state for skip/break control.
69
+
70
+ Args:
71
+ name: Method name being called
72
+
73
+ Returns:
74
+ Callable that merges visitor results with state tracking
75
+
76
+ Raises:
77
+ AttributeError: If not a visit_* method
78
+ """
79
+ if not name.startswith("visit"):
80
+ raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")
81
+
82
+ # Determine if this is a leave hook
83
+ is_leave_hook = name.startswith("visit_leave")
84
+
85
+ def merged_visit_method(path: NodePath):
86
+ for i, visitor in enumerate(self.visitors):
87
+ if is_leave_hook:
88
+ # Leave hook: only call if visitor is active
89
+ if self._is_active(i) and hasattr(visitor, name):
90
+ result = getattr(visitor, name)(path)
91
+ if result is BREAK:
92
+ self._skipping_state[i] = BREAK # Stop this visitor
93
+ # Resume visitor if leaving the skipped node (don't call leave hook)
94
+ elif self._is_skipping_node(i, path):
95
+ self._skipping_state[i] = None # Resume for next nodes
96
+ else:
97
+ # Enter/visit hook: only call if visitor is active
98
+ if self._is_active(i) and hasattr(visitor, name):
99
+ result = getattr(visitor, name)(path)
100
+
101
+ if result is BREAK:
102
+ self._skipping_state[i] = BREAK # Stop this visitor
103
+ elif result is False:
104
+ self._skipping_state[i] = path # Skip descendants
105
+
106
+ # Never return BREAK or False - let traversal continue for all visitors
107
+ return None
108
+
109
+ return merged_visit_method
110
+
111
+ return MergedVisitor(visitors)
@@ -0,0 +1,169 @@
1
+ """NodePath context for traversal."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Literal
5
+
6
+ from jsonpointer import JsonPointer
7
+
8
+ from .introspection import get_yaml_field_name
9
+
10
+
11
+ __all__ = ["NodePath"]
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class NodePath:
16
+ """
17
+ Context for a node during traversal.
18
+
19
+ Provides access to node, parent, ancestry, and path formatting.
20
+ Supports Babel-style sub-traversal via traverse() method.
21
+ """
22
+
23
+ node: Any # Current datamodel object
24
+ parent_path: "NodePath | None" # Reference to parent's NodePath (chain)
25
+ parent_field: str | None # Field name in parent
26
+ parent_key: str | int | None # Key if parent field is list/dict
27
+
28
+ @property
29
+ def parent(self) -> Any | None:
30
+ """Get parent node. Computed from parent_path for convenience."""
31
+ return self.parent_path.node if self.parent_path else None
32
+
33
+ @property
34
+ def ancestors(self) -> tuple[Any, ...]:
35
+ """Get ancestor nodes from root to parent. Computed for convenience."""
36
+ result = []
37
+ current = self.parent_path
38
+ while current is not None:
39
+ result.append(current.node)
40
+ current = current.parent_path
41
+ result.reverse() # Root first
42
+ return tuple(result)
43
+
44
+ def create_child(
45
+ self, node: Any, parent_field: str | None, parent_key: str | int | None
46
+ ) -> "NodePath":
47
+ """
48
+ Create a child NodePath from this path.
49
+
50
+ Helper for creating child paths during traversal.
51
+
52
+ Args:
53
+ node: Child node
54
+ parent_field: Field name in current node (None for dict items to avoid duplicates)
55
+ parent_key: Key if field is list/dict
56
+
57
+ Returns:
58
+ New NodePath for the child
59
+ """
60
+ return NodePath(
61
+ node=node,
62
+ parent_path=self,
63
+ parent_field=parent_field,
64
+ parent_key=parent_key,
65
+ )
66
+
67
+ def traverse(self, visitor) -> None:
68
+ """
69
+ Traverse from this node as root (Babel pattern).
70
+
71
+ Allows convenient sub-traversal with a different visitor.
72
+
73
+ Args:
74
+ visitor: Visitor object with visit_* methods
75
+
76
+ Example:
77
+ class PathItemVisitor:
78
+ def visit_PathItem(self, path):
79
+ # Only traverse GET operations
80
+ if path.node.get:
81
+ operation_visitor = OperationOnlyVisitor()
82
+ get_path = path.create_child(
83
+ node=path.node.get.value,
84
+ parent_field="get",
85
+ parent_key=None
86
+ )
87
+ get_path.traverse(operation_visitor)
88
+ return False # Skip automatic traversal
89
+ """
90
+ from .traversal import traverse
91
+
92
+ traverse(self.node, visitor)
93
+
94
+ def format_path(
95
+ self, *, path_format: Literal["jsonpointer", "jsonpath"] = "jsonpointer"
96
+ ) -> str:
97
+ """
98
+ Format path as RFC 6901 JSON Pointer or RFC 9535 Normalized JSONPath.
99
+
100
+ Args:
101
+ path_format: Output format - "jsonpointer" (default) or "jsonpath"
102
+
103
+ Returns:
104
+ JSONPointer string like "/paths/~1pets/get/responses/200"
105
+ or Normalized JSONPath like "$['paths']['/pets']['get']['responses']['200']"
106
+
107
+ Examples (jsonpointer):
108
+ "" (root)
109
+ "/info"
110
+ "/paths/~1pets/get"
111
+ "/paths/~1users~1{id}/parameters/0"
112
+ "/components/schemas/User/properties/name"
113
+
114
+ Examples (jsonpath):
115
+ "$" (root)
116
+ "$['info']"
117
+ "$['paths']['/pets']['get']"
118
+ "$['paths']['/users/{id}']['parameters'][0]"
119
+ "$['components']['schemas']['User']['properties']['name']"
120
+ """
121
+ # Root node
122
+ if self.parent_path is None:
123
+ return "$" if path_format == "jsonpath" else ""
124
+
125
+ # Walk back collecting all segments
126
+ segments: list[str | int] = []
127
+ current = self
128
+ while current.parent_path is not None:
129
+ # Add in reverse order (key first, then field) because we'll reverse the list
130
+ # This ensures field comes before key in the final path
131
+ if current.parent_key is not None:
132
+ segments.append(current.parent_key)
133
+ if current.parent_field:
134
+ # Convert Python field name to YAML name for output
135
+ parent_class = type(current.parent_path.node)
136
+ yaml_name = get_yaml_field_name(parent_class, current.parent_field)
137
+ segments.append(yaml_name)
138
+ current = current.parent_path
139
+
140
+ segments.reverse() # Root to leaf order
141
+
142
+ if path_format == "jsonpath":
143
+ # RFC 9535 Normalized JSONPath: $['field'][index]['key']
144
+ result = ["$"]
145
+ for segment in segments:
146
+ if isinstance(segment, int):
147
+ # Array index: $[0]
148
+ result.append(f"[{segment}]")
149
+ else:
150
+ # Member name: $['field']
151
+ # Escape single quotes in the string
152
+ escaped = str(segment).replace("'", "\\'")
153
+ result.append(f"['{escaped}']")
154
+ return "".join(result)
155
+
156
+ # RFC 6901 JSON Pointer
157
+ return JsonPointer.from_parts(segments).path
158
+
159
+ def get_root(self) -> Any:
160
+ """
161
+ Get the root node of the tree.
162
+
163
+ Returns:
164
+ Root datamodel object
165
+ """
166
+ current = self
167
+ while current.parent_path is not None:
168
+ current = current.parent_path
169
+ return current.node
@@ -0,0 +1,245 @@
1
+ """Core traversal functionality for low-level OpenAPI datamodels."""
2
+
3
+ from jentic.apitools.openapi.datamodels.low.fields import patterned_fields
4
+
5
+ from .introspection import get_traversable_fields, is_datamodel_node, unwrap_value
6
+ from .path import NodePath
7
+
8
+
9
+ __all__ = ["DataModelLowVisitor", "traverse", "BREAK"]
10
+
11
+
12
+ class _BreakType: ...
13
+
14
+
15
+ BREAK = _BreakType()
16
+
17
+
18
+ class DataModelLowVisitor:
19
+ """
20
+ Optional base class for OpenAPI datamodel visitors.
21
+
22
+ You don't need to inherit from this class - just implement visit_* methods.
23
+ Inheritance is optional and provides no functionality - use for organizational purposes only.
24
+
25
+ Visitor Method Signatures:
26
+ Generic hooks (fire for ALL nodes):
27
+ - visit_enter(path: NodePath) -> None | False | BREAK
28
+ - visit_leave(path: NodePath) -> None | False | BREAK
29
+
30
+ Class-specific hooks (fire for matching node types):
31
+ - visit_ClassName(path: NodePath) -> None | False | BREAK
32
+ - visit_enter_ClassName(path: NodePath) -> None | False | BREAK
33
+ - visit_leave_ClassName(path: NodePath) -> None | False | BREAK
34
+
35
+ Dispatch Order:
36
+ 1. visit_enter(path) - generic enter
37
+ 2. visit_enter_ClassName(path) - specific enter
38
+ 3. visit_ClassName(path) - main visitor
39
+ 4. [child traversal - automatic unless False returned]
40
+ 5. visit_leave_ClassName(path) - specific leave
41
+ 6. visit_leave(path) - generic leave
42
+
43
+ Return Values:
44
+ - None: Continue traversal normally (children visited automatically)
45
+ - False: Skip visiting children of this node
46
+ - BREAK: Stop entire traversal immediately
47
+
48
+ Example (with enter/leave hooks):
49
+ class MyVisitor(DataModelLowVisitor): # Optional inheritance
50
+ def visit_enter_Operation(self, path):
51
+ print(f"Entering: {path.format_path()}")
52
+
53
+ def visit_leave_Operation(self, path):
54
+ print(f"Leaving: {path.format_path()}")
55
+
56
+ class SimpleVisitor: # No inheritance (duck typing works too)
57
+ def visit_Operation(self, path):
58
+ print(f"Found: {path.format_path()}")
59
+ # Children automatically visited
60
+ """
61
+
62
+ pass
63
+
64
+
65
+ def traverse(root, visitor) -> None:
66
+ """
67
+ Traverse OpenAPI datamodel tree using visitor pattern.
68
+
69
+ The visitor can be any object with visit_* methods (duck typing).
70
+ Optionally inherit from DataModelLowVisitor for organizational purposes.
71
+
72
+ Children are automatically traversed unless a visitor method returns False.
73
+ Use enter/leave hooks for pre/post traversal logic.
74
+
75
+ Args:
76
+ root: Root datamodel object (OpenAPI30, OpenAPI31, or any datamodel node)
77
+ visitor: Object with visit_* methods
78
+
79
+ Example:
80
+ # Using enter/leave hooks
81
+ class MyVisitor(DataModelLowVisitor):
82
+ def visit_enter_Operation(self, path):
83
+ print(f"Entering operation: {path.format_path()}")
84
+
85
+ def visit_leave_Operation(self, path):
86
+ print(f"Leaving operation: {path.format_path()}")
87
+
88
+ # Simple visitor (duck typing - no inheritance needed)
89
+ class SimpleVisitor:
90
+ def visit_Operation(self, path):
91
+ print(f"Found operation: {path.format_path()}")
92
+ # Children automatically visited
93
+
94
+ doc = parser.parse(..., return_type=DataModelLow)
95
+ traverse(doc, MyVisitor())
96
+ traverse(doc, SimpleVisitor())
97
+ """
98
+ # Create initial root path
99
+ initial_path = NodePath(
100
+ node=root,
101
+ parent_path=None,
102
+ parent_field=None,
103
+ parent_key=None,
104
+ )
105
+
106
+ # Start traversal
107
+ _visit_node(visitor, initial_path)
108
+
109
+
110
+ def _default_traverse_children(visitor, path: NodePath) -> _BreakType | None:
111
+ """
112
+ Internal child traversal logic.
113
+
114
+ Iterates through traversable fields and visits datamodel children.
115
+ Called automatically during traversal.
116
+
117
+ Args:
118
+ visitor: Visitor object with visit_* methods
119
+ path: Current node path
120
+
121
+ Returns:
122
+ BREAK to stop traversal, None otherwise
123
+ """
124
+ # Get all traversable fields
125
+ for field_name, field_value in get_traversable_fields(path.node):
126
+ unwrapped = unwrap_value(field_value)
127
+
128
+ # Handle single datamodel nodes
129
+ if is_datamodel_node(unwrapped):
130
+ child_path = path.create_child(node=unwrapped, parent_field=field_name, parent_key=None)
131
+ result = _visit_node(visitor, child_path)
132
+ if result is BREAK:
133
+ return BREAK
134
+
135
+ # Handle lists
136
+ elif isinstance(unwrapped, list):
137
+ for idx, item in enumerate(unwrapped):
138
+ if is_datamodel_node(item):
139
+ child_path = path.create_child(
140
+ node=item, parent_field=field_name, parent_key=idx
141
+ )
142
+ result = _visit_node(visitor, child_path)
143
+ if result is BREAK:
144
+ return BREAK
145
+
146
+ # Handle dicts
147
+ elif isinstance(unwrapped, dict):
148
+ # Check if this field is a patterned field
149
+ # Patterned fields (like Paths.paths, Components.schemas) should not
150
+ # add their field name to the path when iterating dict items
151
+ patterned_field_names = patterned_fields(type(path.node))
152
+ is_patterned = field_name in patterned_field_names
153
+
154
+ for key, value in unwrapped.items():
155
+ unwrapped_key = unwrap_value(key)
156
+ unwrapped_value = unwrap_value(value)
157
+
158
+ if is_datamodel_node(unwrapped_value):
159
+ # Dict keys should be str after unwrapping (field names, paths, status codes, etc.)
160
+ assert isinstance(unwrapped_key, (str, int)), (
161
+ f"Expected str or int key, got {type(unwrapped_key)}"
162
+ )
163
+ # For patterned fields, don't include the field name in the path
164
+ # (e.g., Paths.paths is patterned, so /paths/{path-key} not /paths/paths/{path-key})
165
+ dict_field_name: str | None = None if is_patterned else field_name
166
+ child_path = path.create_child(
167
+ node=unwrapped_value,
168
+ parent_field=dict_field_name,
169
+ parent_key=unwrapped_key,
170
+ )
171
+ result = _visit_node(visitor, child_path)
172
+ if result is BREAK:
173
+ return BREAK
174
+
175
+ return None
176
+
177
+
178
+ def _visit_node(visitor, path: NodePath) -> _BreakType | None:
179
+ """
180
+ Visit a single node with the visitor.
181
+
182
+ Handles enter/main/leave dispatch and control flow.
183
+ Duck typed - works with any object that has visit_* methods.
184
+
185
+ Args:
186
+ visitor: Visitor object with visit_* methods
187
+ path: Current node path
188
+
189
+ Returns:
190
+ BREAK to stop traversal, None otherwise
191
+ """
192
+ node_class = path.node.__class__.__name__
193
+
194
+ # Generic enter hook: visit_enter (fires for ALL nodes)
195
+ if hasattr(visitor, "visit_enter"):
196
+ result = visitor.visit_enter(path)
197
+ if result is BREAK:
198
+ return BREAK
199
+ if result is False:
200
+ return None # Skip children, but continue traversal
201
+
202
+ # Try enter hook: visit_enter_ClassName
203
+ enter_method = f"visit_enter_{node_class}"
204
+ if hasattr(visitor, enter_method):
205
+ result = getattr(visitor, enter_method)(path)
206
+ if result is BREAK:
207
+ return BREAK
208
+ if result is False:
209
+ return None # Skip children, but continue traversal
210
+
211
+ # Try main visitor: visit_ClassName
212
+ visit_method = f"visit_{node_class}"
213
+ skip_auto_traverse = False
214
+
215
+ if hasattr(visitor, visit_method):
216
+ result = getattr(visitor, visit_method)(path)
217
+ # Only care about BREAK and False:
218
+ # - BREAK: stop entire traversal
219
+ # - False: skip children of this node
220
+ # - Any other value (None, True, etc.): continue normally
221
+ if result is BREAK:
222
+ return BREAK
223
+ if result is False:
224
+ skip_auto_traverse = True
225
+
226
+ # Automatic child traversal (unless explicitly skipped)
227
+ if not skip_auto_traverse:
228
+ result = _default_traverse_children(visitor, path)
229
+ if result is BREAK:
230
+ return BREAK
231
+
232
+ # Try leave hook: visit_leave_ClassName
233
+ leave_method = f"visit_leave_{node_class}"
234
+ if hasattr(visitor, leave_method):
235
+ result = getattr(visitor, leave_method)(path)
236
+ if result is BREAK:
237
+ return BREAK
238
+
239
+ # Generic leave hook: visit_leave (fires for ALL nodes)
240
+ if hasattr(visitor, "visit_leave"):
241
+ result = visitor.visit_leave(path)
242
+ if result is BREAK:
243
+ return BREAK
244
+
245
+ return None
@@ -0,0 +1,20 @@
1
+ """Generic JSON tree traversal utilities."""
2
+
3
+ from jentic.apitools.openapi.traverse.json.traversal import (
4
+ JSONContainer,
5
+ JSONPath,
6
+ JSONValue,
7
+ PathSeg,
8
+ TraversalNode,
9
+ traverse,
10
+ )
11
+
12
+
13
+ __all__ = [
14
+ "JSONContainer",
15
+ "JSONPath",
16
+ "JSONValue",
17
+ "PathSeg",
18
+ "TraversalNode",
19
+ "traverse",
20
+ ]