jentic-openapi-traverse 1.0.0a22__py3-none-any.whl → 1.0.0a23__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,114 @@
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
+ ]
19
+
20
+
21
+ # Cache of field names to check per class type
22
+ # {Info: ["title", "description", "contact", ...], Operation: [...], ...}
23
+ _FIELD_NAMES_CACHE: dict[type, list[str]] = {}
24
+
25
+
26
+ def get_traversable_fields(node):
27
+ """
28
+ Get all fields that should be traversed in a datamodel node.
29
+
30
+ Uses field metadata (fixed_field, patterned_field) to identify OpenAPI
31
+ specification fields. This leverages the explicit field marking system
32
+ from the datamodels package.
33
+
34
+ Caches field names per class type for performance.
35
+
36
+ Args:
37
+ node: Datamodel node (dataclass instance)
38
+
39
+ Returns:
40
+ List of (field_name, field_value) tuples
41
+ """
42
+ if not is_dataclass(node):
43
+ return []
44
+
45
+ node_class = type(node)
46
+
47
+ # Get or compute field names for this class
48
+ if node_class not in _FIELD_NAMES_CACHE:
49
+ # Get all OpenAPI spec fields (fixed + patterned)
50
+ fixed = fixed_fields(node_class)
51
+ patterned = patterned_fields(node_class)
52
+ # Combine and extract field names
53
+ field_names = list(fixed.keys()) + list(patterned.keys())
54
+ _FIELD_NAMES_CACHE[node_class] = field_names
55
+
56
+ # Use cached field names
57
+ result = []
58
+ for field_name in _FIELD_NAMES_CACHE[node_class]:
59
+ value = getattr(node, field_name, None)
60
+
61
+ # Skip None values
62
+ if value is None:
63
+ continue
64
+
65
+ # Check if it's traversable
66
+ unwrapped = unwrap_value(value)
67
+
68
+ # Skip scalar primitives
69
+ if isinstance(unwrapped, (str, int, float, bool, type(None))):
70
+ continue
71
+
72
+ result.append((field_name, value))
73
+
74
+ return result
75
+
76
+
77
+ def unwrap_value(value: Any) -> Any:
78
+ """
79
+ Unwrap FieldSource/ValueSource/KeySource to get actual value.
80
+
81
+ Wrapper types (FieldSource, ValueSource, KeySource) are used to preserve
82
+ source location information for field values. This function extracts the
83
+ actual value from these wrappers.
84
+
85
+ This excludes datamodel nodes like Example which may have .value field
86
+ but are not wrapper types.
87
+
88
+ Args:
89
+ value: Potentially wrapped value
90
+
91
+ Returns:
92
+ Unwrapped value, or original if not wrapped
93
+ """
94
+ # Check if it's a wrapper type
95
+ if isinstance(value, (FieldSource, ValueSource, KeySource)):
96
+ return value.value
97
+ return value
98
+
99
+
100
+ def is_datamodel_node(value):
101
+ """
102
+ Check if value is a low-level datamodel object.
103
+
104
+ Low-level datamodels are distinguished by having a root_node field,
105
+ which contains the YAML source location information. This excludes
106
+ wrapper types (FieldSource, ValueSource, KeySource) and other dataclasses.
107
+
108
+ Args:
109
+ value: Value to check
110
+
111
+ Returns:
112
+ True if it's a low-level datamodel node, False otherwise
113
+ """
114
+ 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,145 @@
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
+
9
+ __all__ = ["NodePath"]
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class NodePath:
14
+ """
15
+ Context for a node during traversal.
16
+
17
+ Provides access to node, parent, ancestry, and path formatting.
18
+ Supports Babel-style sub-traversal via traverse() method.
19
+ """
20
+
21
+ node: Any # Current datamodel object
22
+ parent: Any | None # Parent datamodel object
23
+ parent_field: str | None # Field name in parent
24
+ parent_key: str | int | None # Key if parent field is list/dict
25
+ ancestors: tuple[Any, ...] # Tuple of ancestor nodes (root first)
26
+
27
+ def create_child(
28
+ self, node: Any, parent_field: str, parent_key: str | int | None
29
+ ) -> "NodePath":
30
+ """
31
+ Create a child NodePath from this path.
32
+
33
+ Helper for creating child paths during traversal.
34
+
35
+ Args:
36
+ node: Child node
37
+ parent_field: Field name in current node
38
+ parent_key: Key if field is list/dict
39
+
40
+ Returns:
41
+ New NodePath for the child
42
+ """
43
+ return NodePath(
44
+ node=node,
45
+ parent=self.node,
46
+ parent_field=parent_field,
47
+ parent_key=parent_key,
48
+ ancestors=self.ancestors + (self.node,),
49
+ )
50
+
51
+ def traverse(self, visitor) -> None:
52
+ """
53
+ Traverse from this node as root (Babel pattern).
54
+
55
+ Allows convenient sub-traversal with a different visitor.
56
+
57
+ Args:
58
+ visitor: Visitor object with visit_* methods
59
+
60
+ Example:
61
+ class PathItemVisitor:
62
+ def visit_PathItem(self, path):
63
+ # Only traverse GET operations
64
+ if path.node.get:
65
+ operation_visitor = OperationOnlyVisitor()
66
+ get_path = path.create_child(
67
+ node=path.node.get.value,
68
+ parent_field="get",
69
+ parent_key=None
70
+ )
71
+ get_path.traverse(operation_visitor)
72
+ return False # Skip automatic traversal
73
+ """
74
+ from .traversal import traverse
75
+
76
+ traverse(self.node, visitor)
77
+
78
+ def format_path(
79
+ self, *, path_format: Literal["jsonpointer", "jsonpath"] = "jsonpointer"
80
+ ) -> str:
81
+ """
82
+ Format path as RFC 6901 JSON Pointer or RFC 9535 Normalized JSONPath.
83
+
84
+ Args:
85
+ path_format: Output format - "jsonpointer" (default) or "jsonpath"
86
+
87
+ Returns:
88
+ JSONPointer string like "/paths/~1pets/get/responses/200"
89
+ or Normalized JSONPath like "$['paths']['/pets']['get']['responses']['200']"
90
+
91
+ Examples (jsonpointer):
92
+ "" (root)
93
+ "/info"
94
+ "/paths/~1pets/get"
95
+ "/paths/~1users~1{id}/parameters/0"
96
+ "/components/schemas/User/properties/name"
97
+
98
+ Examples (jsonpath):
99
+ "$" (root)
100
+ "$['info']"
101
+ "$['paths']['/pets']['get']"
102
+ "$['paths']['/users/{id}']['parameters'][0]"
103
+ "$['components']['schemas']['User']['properties']['name']"
104
+ """
105
+ # Root node
106
+ if not self.ancestors and self.parent_field is None:
107
+ return "$" if path_format == "jsonpath" else ""
108
+
109
+ # Build parts list
110
+ parts: list[str | int] = []
111
+
112
+ # This is a simplified implementation that only captures one level
113
+ # A full implementation would need to walk back through ancestors
114
+ if self.parent_field:
115
+ parts.append(self.parent_field)
116
+
117
+ if self.parent_key is not None:
118
+ # Both int (array index) and str (object key) work with from_parts
119
+ parts.append(self.parent_key)
120
+
121
+ if path_format == "jsonpath":
122
+ # RFC 9535 Normalized JSONPath: $['field'][index]['key']
123
+ segments = ["$"]
124
+ for part in parts:
125
+ if isinstance(part, int):
126
+ # Array index: $[0]
127
+ segments.append(f"[{part}]")
128
+ else:
129
+ # Member name: $['field']
130
+ # Escape single quotes in the string
131
+ escaped = str(part).replace("'", "\\'")
132
+ segments.append(f"['{escaped}']")
133
+ return "".join(segments)
134
+
135
+ # RFC 6901 JSON Pointer
136
+ return JsonPointer.from_parts(parts).path
137
+
138
+ def get_root(self) -> Any:
139
+ """
140
+ Get the root node of the tree.
141
+
142
+ Returns:
143
+ Root datamodel object
144
+ """
145
+ return self.ancestors[0] if self.ancestors else self.node
@@ -0,0 +1,235 @@
1
+ """Core traversal functionality for low-level OpenAPI datamodels."""
2
+
3
+ from .introspection import get_traversable_fields, is_datamodel_node, unwrap_value
4
+ from .path import NodePath
5
+
6
+
7
+ __all__ = ["DataModelLowVisitor", "traverse", "BREAK"]
8
+
9
+
10
+ class _BreakType: ...
11
+
12
+
13
+ BREAK = _BreakType()
14
+
15
+
16
+ class DataModelLowVisitor:
17
+ """
18
+ Optional base class for OpenAPI datamodel visitors.
19
+
20
+ You don't need to inherit from this class - just implement visit_* methods.
21
+ Inheritance is optional and provides no functionality - use for organizational purposes only.
22
+
23
+ Visitor Method Signatures:
24
+ Generic hooks (fire for ALL nodes):
25
+ - visit_enter(path: NodePath) -> None | False | BREAK
26
+ - visit_leave(path: NodePath) -> None | False | BREAK
27
+
28
+ Class-specific hooks (fire for matching node types):
29
+ - visit_ClassName(path: NodePath) -> None | False | BREAK
30
+ - visit_enter_ClassName(path: NodePath) -> None | False | BREAK
31
+ - visit_leave_ClassName(path: NodePath) -> None | False | BREAK
32
+
33
+ Dispatch Order:
34
+ 1. visit_enter(path) - generic enter
35
+ 2. visit_enter_ClassName(path) - specific enter
36
+ 3. visit_ClassName(path) - main visitor
37
+ 4. [child traversal - automatic unless False returned]
38
+ 5. visit_leave_ClassName(path) - specific leave
39
+ 6. visit_leave(path) - generic leave
40
+
41
+ Return Values:
42
+ - None: Continue traversal normally (children visited automatically)
43
+ - False: Skip visiting children of this node
44
+ - BREAK: Stop entire traversal immediately
45
+
46
+ Example (with enter/leave hooks):
47
+ class MyVisitor(DataModelLowVisitor): # Optional inheritance
48
+ def visit_enter_Operation(self, path):
49
+ print(f"Entering: {path.format_path()}")
50
+
51
+ def visit_leave_Operation(self, path):
52
+ print(f"Leaving: {path.format_path()}")
53
+
54
+ class SimpleVisitor: # No inheritance (duck typing works too)
55
+ def visit_Operation(self, path):
56
+ print(f"Found: {path.format_path()}")
57
+ # Children automatically visited
58
+ """
59
+
60
+ pass
61
+
62
+
63
+ def traverse(root, visitor) -> None:
64
+ """
65
+ Traverse OpenAPI datamodel tree using visitor pattern.
66
+
67
+ The visitor can be any object with visit_* methods (duck typing).
68
+ Optionally inherit from DataModelLowVisitor for organizational purposes.
69
+
70
+ Children are automatically traversed unless a visitor method returns False.
71
+ Use enter/leave hooks for pre/post traversal logic.
72
+
73
+ Args:
74
+ root: Root datamodel object (OpenAPI30, OpenAPI31, or any datamodel node)
75
+ visitor: Object with visit_* methods
76
+
77
+ Example:
78
+ # Using enter/leave hooks
79
+ class MyVisitor(DataModelLowVisitor):
80
+ def visit_enter_Operation(self, path):
81
+ print(f"Entering operation: {path.format_path()}")
82
+
83
+ def visit_leave_Operation(self, path):
84
+ print(f"Leaving operation: {path.format_path()}")
85
+
86
+ # Simple visitor (duck typing - no inheritance needed)
87
+ class SimpleVisitor:
88
+ def visit_Operation(self, path):
89
+ print(f"Found operation: {path.format_path()}")
90
+ # Children automatically visited
91
+
92
+ doc = parser.parse(..., return_type=DataModelLow)
93
+ traverse(doc, MyVisitor())
94
+ traverse(doc, SimpleVisitor())
95
+ """
96
+ # Create initial root path
97
+ initial_path = NodePath(
98
+ node=root,
99
+ parent=None,
100
+ parent_field=None,
101
+ parent_key=None,
102
+ ancestors=(),
103
+ )
104
+
105
+ # Start traversal
106
+ _visit_node(visitor, initial_path)
107
+
108
+
109
+ def _default_traverse_children(visitor, path: NodePath) -> _BreakType | None:
110
+ """
111
+ Internal child traversal logic.
112
+
113
+ Iterates through traversable fields and visits datamodel children.
114
+ Called automatically during traversal.
115
+
116
+ Args:
117
+ visitor: Visitor object with visit_* methods
118
+ path: Current node path
119
+
120
+ Returns:
121
+ BREAK to stop traversal, None otherwise
122
+ """
123
+ # Get all traversable fields
124
+ for field_name, field_value in get_traversable_fields(path.node):
125
+ unwrapped = unwrap_value(field_value)
126
+
127
+ # Handle single datamodel nodes
128
+ if is_datamodel_node(unwrapped):
129
+ child_path = path.create_child(node=unwrapped, parent_field=field_name, parent_key=None)
130
+ result = _visit_node(visitor, child_path)
131
+ if result is BREAK:
132
+ return BREAK
133
+
134
+ # Handle lists
135
+ elif isinstance(unwrapped, list):
136
+ for idx, item in enumerate(unwrapped):
137
+ if is_datamodel_node(item):
138
+ child_path = path.create_child(
139
+ node=item, parent_field=field_name, parent_key=idx
140
+ )
141
+ result = _visit_node(visitor, child_path)
142
+ if result is BREAK:
143
+ return BREAK
144
+
145
+ # Handle dicts
146
+ elif isinstance(unwrapped, dict):
147
+ for key, value in unwrapped.items():
148
+ unwrapped_key = unwrap_value(key)
149
+ unwrapped_value = unwrap_value(value)
150
+
151
+ if is_datamodel_node(unwrapped_value):
152
+ # Dict keys should be str after unwrapping (field names, paths, status codes, etc.)
153
+ assert isinstance(unwrapped_key, (str, int)), (
154
+ f"Expected str or int key, got {type(unwrapped_key)}"
155
+ )
156
+ child_path = path.create_child(
157
+ node=unwrapped_value,
158
+ parent_field=field_name,
159
+ parent_key=unwrapped_key,
160
+ )
161
+ result = _visit_node(visitor, child_path)
162
+ if result is BREAK:
163
+ return BREAK
164
+
165
+ return None
166
+
167
+
168
+ def _visit_node(visitor, path: NodePath) -> _BreakType | None:
169
+ """
170
+ Visit a single node with the visitor.
171
+
172
+ Handles enter/main/leave dispatch and control flow.
173
+ Duck typed - works with any object that has visit_* methods.
174
+
175
+ Args:
176
+ visitor: Visitor object with visit_* methods
177
+ path: Current node path
178
+
179
+ Returns:
180
+ BREAK to stop traversal, None otherwise
181
+ """
182
+ node_class = path.node.__class__.__name__
183
+
184
+ # Generic enter hook: visit_enter (fires for ALL nodes)
185
+ if hasattr(visitor, "visit_enter"):
186
+ result = visitor.visit_enter(path)
187
+ if result is BREAK:
188
+ return BREAK
189
+ if result is False:
190
+ return None # Skip children, but continue traversal
191
+
192
+ # Try enter hook: visit_enter_ClassName
193
+ enter_method = f"visit_enter_{node_class}"
194
+ if hasattr(visitor, enter_method):
195
+ result = getattr(visitor, enter_method)(path)
196
+ if result is BREAK:
197
+ return BREAK
198
+ if result is False:
199
+ return None # Skip children, but continue traversal
200
+
201
+ # Try main visitor: visit_ClassName
202
+ visit_method = f"visit_{node_class}"
203
+ skip_auto_traverse = False
204
+
205
+ if hasattr(visitor, visit_method):
206
+ result = getattr(visitor, visit_method)(path)
207
+ # Only care about BREAK and False:
208
+ # - BREAK: stop entire traversal
209
+ # - False: skip children of this node
210
+ # - Any other value (None, True, etc.): continue normally
211
+ if result is BREAK:
212
+ return BREAK
213
+ if result is False:
214
+ skip_auto_traverse = True
215
+
216
+ # Automatic child traversal (unless explicitly skipped)
217
+ if not skip_auto_traverse:
218
+ result = _default_traverse_children(visitor, path)
219
+ if result is BREAK:
220
+ return BREAK
221
+
222
+ # Try leave hook: visit_leave_ClassName
223
+ leave_method = f"visit_leave_{node_class}"
224
+ if hasattr(visitor, leave_method):
225
+ result = getattr(visitor, leave_method)(path)
226
+ if result is BREAK:
227
+ return BREAK
228
+
229
+ # Generic leave hook: visit_leave (fires for ALL nodes)
230
+ if hasattr(visitor, "visit_leave"):
231
+ result = visitor.visit_leave(path)
232
+ if result is BREAK:
233
+ return BREAK
234
+
235
+ return None
@@ -0,0 +1,491 @@
1
+ Metadata-Version: 2.4
2
+ Name: jentic-openapi-traverse
3
+ Version: 1.0.0a23
4
+ Summary: Jentic OpenAPI Traversal Utilities
5
+ Author: Jentic
6
+ Author-email: Jentic <hello@jentic.com>
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ License-File: NOTICE
10
+ Requires-Dist: jentic-openapi-datamodels~=1.0.0a23
11
+ Requires-Dist: jsonpointer~=3.0.0
12
+ Requires-Python: >=3.11
13
+ Project-URL: Homepage, https://github.com/jentic/jentic-openapi-tools
14
+ Description-Content-Type: text/markdown
15
+
16
+ # jentic-openapi-traverse
17
+
18
+ A Python library for traversing OpenAPI documents. This package is part of the Jentic OpenAPI Tools ecosystem and provides two types of traversal:
19
+
20
+ 1. **Datamodel Traversal** - OpenAPI-aware semantic traversal with visitor pattern
21
+ 2. **JSON Traversal** - Generic depth-first traversal of JSON-like structures
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install jentic-openapi-traverse
27
+ ```
28
+
29
+ **Prerequisites:**
30
+ - Python 3.11+
31
+
32
+ ---
33
+
34
+ ## Datamodel Traversal
35
+
36
+ OpenAPI-aware semantic traversal using the visitor pattern. Works with low-level datamodels from `jentic-openapi-datamodels` package, preserving source location information and providing structured access to OpenAPI nodes.
37
+
38
+ ### Quick Start
39
+
40
+ ```python
41
+ from jentic.apitools.openapi.parser.core import OpenAPIParser
42
+ from jentic.apitools.openapi.traverse.datamodels.low import traverse, DataModelLowVisitor
43
+
44
+ # Parse OpenAPI document
45
+ parser = OpenAPIParser("datamodel-low")
46
+ doc = parser.parse("file:///path/to/openapi.yaml")
47
+
48
+ # Create visitor
49
+ class OperationCollector(DataModelLowVisitor):
50
+ def __init__(self):
51
+ self.operations = []
52
+
53
+ def visit_Operation(self, path):
54
+ self.operations.append({
55
+ "path": path.format_path(path_format="jsonpointer"),
56
+ "operation_id": path.node.operation_id.value if path.node.operation_id else None
57
+ })
58
+
59
+ # Traverse
60
+ visitor = OperationCollector()
61
+ traverse(doc, visitor)
62
+
63
+ print(f"Found {len(visitor.operations)} operations")
64
+ ```
65
+
66
+ ### Visitor Pattern
67
+
68
+ The datamodel traversal uses a flexible visitor pattern with multiple hook types:
69
+
70
+ #### Hook Methods
71
+
72
+ **Generic hooks** (fire for ALL nodes):
73
+ - `visit_enter(path)` - Called before processing any node
74
+ - `visit_leave(path)` - Called after processing any node and its children
75
+
76
+ **Class-specific hooks** (fire for matching node types):
77
+ - `visit_ClassName(path)` - Main visitor for specific node type
78
+ - `visit_enter_ClassName(path)` - Called before visit_ClassName
79
+ - `visit_leave_ClassName(path)` - Called after children are visited
80
+
81
+ #### Dispatch Order
82
+
83
+ For each node, hooks are called in this order:
84
+ 1. `visit_enter(path)` - generic enter
85
+ 2. `visit_enter_ClassName(path)` - specific enter
86
+ 3. `visit_ClassName(path)` - main visitor
87
+ 4. [child traversal - automatic unless skipped]
88
+ 5. `visit_leave_ClassName(path)` - specific leave
89
+ 6. `visit_leave(path)` - generic leave
90
+
91
+ #### Control Flow
92
+
93
+ Visitor methods control traversal by their return value:
94
+
95
+ - `None` (or no return) - **Continue normally** (children visited automatically)
96
+ - `False` - **Skip children** of this node (but continue to siblings)
97
+ - `BREAK` - **Stop entire traversal** immediately
98
+
99
+ ```python
100
+ from jentic.apitools.openapi.traverse.datamodels.low import traverse, BREAK
101
+
102
+ class ControlFlowExample:
103
+ def visit_PathItem(self, path):
104
+ if path.parent_key == "/internal":
105
+ return False # Skip internal paths and their children
106
+
107
+ def visit_Operation(self, path):
108
+ if some_error_condition:
109
+ return BREAK # Stop entire traversal
110
+ ```
111
+
112
+ ### NodePath Context
113
+
114
+ Every visitor method receives a `NodePath` object with context about the current node:
115
+
116
+ ```python
117
+ class PathInspector:
118
+ def visit_Operation(self, path):
119
+ # Current node
120
+ print(f"Node: {path.node.__class__.__name__}")
121
+
122
+ # Parent information
123
+ print(f"Parent field: {path.parent_field}") # e.g., "get", "post"
124
+ print(f"Parent key: {path.parent_key}") # e.g., "/users" (for path items)
125
+
126
+ # Ancestry
127
+ print(f"Ancestors: {len(path.ancestors)}")
128
+ root = path.get_root()
129
+
130
+ # Path formatting
131
+ print(f"JSONPointer: {path.format_path()}") # /paths/~1users/get
132
+ print(f"JSONPath: {path.format_path(path_format='jsonpath')}") # $['paths']['/users']['get']
133
+ ```
134
+
135
+ ### Enter/Leave Hooks
136
+
137
+ Use enter/leave hooks for pre/post processing logic:
138
+
139
+ ```python
140
+ class DepthTracker(DataModelLowVisitor):
141
+ def __init__(self):
142
+ self.current_depth = 0
143
+ self.max_depth = 0
144
+
145
+ def visit_enter(self, path):
146
+ self.current_depth += 1
147
+ self.max_depth = max(self.max_depth, self.current_depth)
148
+ print(" " * self.current_depth + f"Entering {path.node.__class__.__name__}")
149
+
150
+ def visit_leave(self, path):
151
+ print(" " * self.current_depth + f"Leaving {path.node.__class__.__name__}")
152
+ self.current_depth -= 1
153
+ ```
154
+
155
+ ### Examples
156
+
157
+ #### Collecting All Schemas
158
+
159
+ ```python
160
+ class SchemaCollector(DataModelLowVisitor):
161
+ def __init__(self):
162
+ self.schemas = {}
163
+
164
+ def visit_Schema(self, path):
165
+ schema_name = path.parent_key if path.parent_field == "schemas" else None
166
+ if schema_name:
167
+ self.schemas[schema_name] = path.node
168
+
169
+ visitor = SchemaCollector()
170
+ traverse(doc, visitor)
171
+ print(f"Found {len(visitor.schemas)} schemas")
172
+ ```
173
+
174
+ #### Validating Security Requirements
175
+
176
+ ```python
177
+ class SecurityValidator(DataModelLowVisitor):
178
+ def __init__(self):
179
+ self.errors = []
180
+
181
+ def visit_Operation(self, path):
182
+ if not path.node.security:
183
+ self.errors.append(f"Missing security at {path.format_path()}")
184
+
185
+ def visit_SecurityRequirement(self, path):
186
+ # Validate security requirement
187
+ if not path.node.schemes:
188
+ self.errors.append(f"Empty security requirement at {path.format_path()}")
189
+
190
+ visitor = SecurityValidator()
191
+ traverse(doc, visitor)
192
+ if visitor.errors:
193
+ for error in visitor.errors:
194
+ print(f"Security error: {error}")
195
+ ```
196
+
197
+ #### Finding Deprecated Operations
198
+
199
+ ```python
200
+ class DeprecatedFinder:
201
+ def __init__(self):
202
+ self.deprecated_ops = []
203
+
204
+ def visit_Operation(self, path):
205
+ if path.node.deprecated and path.node.deprecated.value:
206
+ self.deprecated_ops.append({
207
+ "path": path.format_path(),
208
+ "operation_id": path.node.operation_id.value if path.node.operation_id else None,
209
+ "method": path.parent_field
210
+ })
211
+ return False # Skip children (we don't need to go deeper)
212
+
213
+ visitor = DeprecatedFinder()
214
+ traverse(doc, visitor)
215
+ ```
216
+
217
+ #### Early Exit on Error
218
+
219
+ ```python
220
+ class ErrorDetector(DataModelLowVisitor):
221
+ def __init__(self):
222
+ self.error_found = False
223
+ self.error_location = None
224
+
225
+ def visit_Operation(self, path):
226
+ if not path.node.responses:
227
+ self.error_found = True
228
+ self.error_location = path.format_path()
229
+ return BREAK # Stop traversal immediately
230
+ ```
231
+
232
+ ### Merging Multiple Visitors
233
+
234
+ Run multiple visitors in a single traversal pass (parallel visitation) using `merge_visitors`:
235
+
236
+ ```python
237
+ from jentic.apitools.openapi.traverse.datamodels.low import merge_visitors
238
+
239
+ # Create separate visitors
240
+ schema_collector = SchemaCollector()
241
+ security_validator = SecurityValidator()
242
+ deprecated_finder = DeprecatedFinder()
243
+
244
+ # Merge and traverse once
245
+ merged = merge_visitors(schema_collector, security_validator, deprecated_finder)
246
+ traverse(doc, merged)
247
+
248
+ # Each visitor maintains independent state
249
+ print(f"Schemas: {len(schema_collector.schemas)}")
250
+ print(f"Security errors: {len(security_validator.errors)}")
251
+ print(f"Deprecated: {len(deprecated_finder.deprecated_ops)}")
252
+ ```
253
+
254
+ **Per-Visitor Control Flow:**
255
+ - Each visitor can independently skip subtrees or break
256
+ - If `visitor1` returns `False`, only `visitor1` skips children
257
+ - Other visitors continue normally
258
+ - This follows ApiDOM's per-visitor semantics
259
+
260
+ ### Duck Typing
261
+
262
+ You don't need to inherit from `DataModelLowVisitor` - duck typing works:
263
+
264
+ ```python
265
+ class SimpleCounter: # No inheritance
266
+ def __init__(self):
267
+ self.count = 0
268
+
269
+ def visit_Operation(self, path):
270
+ self.count += 1
271
+
272
+ visitor = SimpleCounter()
273
+ traverse(doc, visitor)
274
+ ```
275
+
276
+ The `DataModelLowVisitor` base class is optional and provides no functionality - it's purely for organizational purposes.
277
+
278
+ ### API Reference
279
+
280
+ #### `traverse(root, visitor) -> None`
281
+
282
+ Traverse OpenAPI datamodel tree using visitor pattern.
283
+
284
+ **Parameters:**
285
+ - `root` - Root datamodel object (OpenAPI30, OpenAPI31, or any datamodel node)
286
+ - `visitor` - Object with `visit_*` methods (duck typing)
287
+
288
+ **Returns:**
289
+ - None (traversal is side-effect based)
290
+
291
+ #### `BREAK`
292
+
293
+ Sentinel value to stop traversal immediately. Return this from any visitor method.
294
+
295
+ ```python
296
+ from jentic.apitools.openapi.traverse.datamodels.low import BREAK
297
+
298
+ def visit_Operation(self, path):
299
+ if should_stop:
300
+ return BREAK
301
+ ```
302
+
303
+ #### `merge_visitors(*visitors) -> object`
304
+
305
+ Merge multiple visitors into one composite visitor.
306
+
307
+ **Parameters:**
308
+ - `*visitors` - Variable number of visitor objects
309
+
310
+ **Returns:**
311
+ - Composite visitor object with per-visitor state tracking
312
+
313
+
314
+ ## JSON Traversal
315
+
316
+ Generic depth-first traversal of any JSON-like structure (dicts, lists, scalars).
317
+ Works with raw parsed OpenAPI documents or any other JSON data.
318
+
319
+ ### Quick Start
320
+
321
+ ```python
322
+ from jentic.apitools.openapi.traverse.json import traverse
323
+
324
+ # Traverse a nested structure
325
+ data = {
326
+ "openapi": "3.1.0",
327
+ "info": {"title": "My API", "version": "1.0.0"},
328
+ "paths": {
329
+ "/users": {
330
+ "get": {"summary": "List users"}
331
+ }
332
+ }
333
+ }
334
+
335
+ # Walk all nodes
336
+ for node in traverse(data):
337
+ print(f"{node.format_path()}: {node.value}")
338
+ ```
339
+
340
+ Output:
341
+ ```
342
+ openapi: 3.1.0
343
+ info: {'title': 'My API', 'version': '1.0.0'}
344
+ info.title: My API
345
+ info.version: 1.0.0
346
+ paths: {'/users': {'get': {'summary': 'List users'}}}
347
+ paths./users: {'get': {'summary': 'List users'}}
348
+ paths./users.get: {'summary': 'List users'}
349
+ paths./users.get.summary: List users
350
+ ```
351
+
352
+ ### Working with Paths
353
+
354
+ ```python
355
+ from jentic.apitools.openapi.traverse.json import traverse
356
+
357
+ data = {
358
+ "users": [
359
+ {"name": "Alice", "email": "alice@example.com"},
360
+ {"name": "Bob", "email": "bob@example.com"}
361
+ ]
362
+ }
363
+
364
+ for node in traverse(data):
365
+ # Access path information
366
+ print(f"Path: {node.path}")
367
+ print(f"Segment: {node.segment}")
368
+ print(f"Full path: {node.full_path}")
369
+ print(f"Formatted: {node.format_path()}")
370
+ print(f"Depth: {len(node.ancestors)}")
371
+ print()
372
+ ```
373
+
374
+ ### Custom Path Formatting
375
+
376
+ ```python
377
+ for node in traverse(data):
378
+ # Default dot separator
379
+ print(node.format_path()) # e.g., "paths./users.get.summary"
380
+
381
+ # Custom separator
382
+ print(node.format_path(separator="/")) # e.g., "paths//users/get/summary"
383
+ ```
384
+
385
+ ### Finding Specific Nodes
386
+
387
+ ```python
388
+ # Find all $ref references in a document
389
+ refs = [
390
+ node.value["$ref"]
391
+ for node in traverse(openapi_doc)
392
+ if isinstance(node.value, dict) and "$ref" in node.value
393
+ ]
394
+
395
+ # Find all nodes at a specific path segment
396
+ schemas = [
397
+ node.value
398
+ for node in traverse(openapi_doc)
399
+ if node.segment == "schema"
400
+ ]
401
+
402
+ # Find deeply nested values
403
+ response_descriptions = [
404
+ node.value
405
+ for node in traverse(openapi_doc)
406
+ if node.segment == "description" and "responses" in node.path
407
+ ]
408
+ ```
409
+
410
+ ### API Reference
411
+
412
+ #### `traverse(root: JSONValue) -> Iterator[TraversalNode]`
413
+
414
+ Performs depth-first traversal of a JSON-like structure.
415
+
416
+ **Parameters:**
417
+ - `root`: The data structure to traverse (dict, list, or scalar)
418
+
419
+ **Returns:**
420
+ - Iterator of `TraversalNode` objects
421
+
422
+ **Yields:**
423
+ - For dicts: one node per key-value pair
424
+ - For lists: one node per index-item pair
425
+ - Scalars at root don't yield nodes (but are accessible via parent nodes)
426
+
427
+ #### `TraversalNode`
428
+
429
+ Immutable dataclass representing a node encountered during traversal.
430
+
431
+ **Attributes:**
432
+ - `path: JSONPath` - Path from root to the parent container (tuple of segments)
433
+ - `parent: JSONContainer` - The parent container (dict or list)
434
+ - `segment: PathSeg` - The key (for dicts) or index (for lists) within parent
435
+ - `value: JSONValue` - The actual value at `parent[segment]`
436
+ - `ancestors: tuple[JSONValue, ...]` - Ordered tuple of values from root down to (but not including) parent
437
+
438
+ **Properties:**
439
+ - `full_path: JSONPath` - Complete path from root to this value (`path + (segment,)`)
440
+
441
+ **Methods:**
442
+ - `format_path(separator: str = ".") -> str` - Format the full path as a human-readable string
443
+
444
+ ### Usage Examples
445
+
446
+ #### Collecting All Schemas
447
+
448
+ ```python
449
+ from jentic.apitools.openapi.traverse.json import traverse
450
+
451
+ def collect_schemas(openapi_doc):
452
+ """Collect all schema objects from an OpenAPI document."""
453
+ schemas = []
454
+
455
+ for node in traverse(openapi_doc):
456
+ if node.segment == "schema" and isinstance(node.value, dict):
457
+ schemas.append({
458
+ "path": node.format_path(),
459
+ "schema": node.value
460
+ })
461
+
462
+ return schemas
463
+ ```
464
+
465
+
466
+ #### Analyzing Document Structure
467
+
468
+ ```python
469
+ def analyze_depth(data):
470
+ """Analyze the depth distribution of a document."""
471
+ max_depth = 0
472
+ depth_counts = {}
473
+
474
+ for node in traverse(data):
475
+ depth = len(node.ancestors)
476
+ max_depth = max(max_depth, depth)
477
+ depth_counts[depth] = depth_counts.get(depth, 0) + 1
478
+
479
+ return {
480
+ "max_depth": max_depth,
481
+ "depth_distribution": depth_counts
482
+ }
483
+ ```
484
+
485
+ ### Testing
486
+
487
+ The package includes comprehensive test coverage for JSON traversal:
488
+
489
+ ```bash
490
+ uv run --package jentic-openapi-traverse pytest packages/jentic-openapi-traverse/tests -v
491
+ ```
@@ -0,0 +1,14 @@
1
+ jentic/apitools/openapi/traverse/datamodels/low/__init__.py,sha256=M0xRiYA2ErksACo8bKSVIu6PVx9aiYTDCHEkcTnmXAA,277
2
+ jentic/apitools/openapi/traverse/datamodels/low/introspection.py,sha256=_QzvTnxrSlyRc_QIajyEkaKEwyng7t1y_qx9dEXxwg8,3216
3
+ jentic/apitools/openapi/traverse/datamodels/low/merge.py,sha256=Z7OpX14y740oC2ML_ylTE0TTh-ZbrEaliMU9s1bDFfM,4411
4
+ jentic/apitools/openapi/traverse/datamodels/low/path.py,sha256=MUqqxzEjOQGzt-TPlhWyh40xXG5B3ABYKvMWxmKgU1Q,4715
5
+ jentic/apitools/openapi/traverse/datamodels/low/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ jentic/apitools/openapi/traverse/datamodels/low/traversal.py,sha256=aAObD_JFRnD2I6EqLgZo3KFL3jkoRxarMcnUqZeMpm0,8090
7
+ jentic/apitools/openapi/traverse/json/__init__.py,sha256=1euUmpZviE_ECtpXYchpO8hZito2BINPjfSHMNqAU8k,326
8
+ jentic/apitools/openapi/traverse/json/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ jentic/apitools/openapi/traverse/json/traversal.py,sha256=1ouszn4S29X0iJaMxxb1neyClbWXqIKwFGhHROcpBSI,3524
10
+ jentic_openapi_traverse-1.0.0a23.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
11
+ jentic_openapi_traverse-1.0.0a23.dist-info/licenses/NOTICE,sha256=pAOGW-rGw9KNc2cuuLWZkfx0GSTV4TicbgBKZSLPMIs,168
12
+ jentic_openapi_traverse-1.0.0a23.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
13
+ jentic_openapi_traverse-1.0.0a23.dist-info/METADATA,sha256=mR38IoYfZFJ7_V-sswXIbQyxUbFypNK6V6q4lrge5EI,13572
14
+ jentic_openapi_traverse-1.0.0a23.dist-info/RECORD,,
@@ -1,209 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: jentic-openapi-traverse
3
- Version: 1.0.0a22
4
- Summary: Jentic OpenAPI Traversal Utilities
5
- Author: Jentic
6
- Author-email: Jentic <hello@jentic.com>
7
- License-Expression: Apache-2.0
8
- License-File: LICENSE
9
- License-File: NOTICE
10
- Requires-Python: >=3.11
11
- Project-URL: Homepage, https://github.com/jentic/jentic-openapi-tools
12
- Description-Content-Type: text/markdown
13
-
14
- # jentic-openapi-traverse
15
-
16
- A Python library for traversing OpenAPI documents. This package is part of the Jentic OpenAPI Tools ecosystem and provides two types of traversal:
17
-
18
- 1. **JSON Traversal** (current) - Generic depth-first traversal of JSON-like structures
19
- 2. **Datamodel Traversal** (planned) - OpenAPI-aware semantic traversal with visitor pattern
20
-
21
- ## Installation
22
-
23
- ```bash
24
- pip install jentic-openapi-traverse
25
- ```
26
-
27
- **Prerequisites:**
28
- - Python 3.11+
29
-
30
- ---
31
-
32
- ## JSON Traversal (Current Implementation)
33
-
34
- Generic depth-first traversal of any JSON-like structure (dicts, lists, scalars).
35
- Works with raw parsed OpenAPI documents or any other JSON data.
36
-
37
- ### Quick Start
38
-
39
- ```python
40
- from jentic.apitools.openapi.traverse.json import traverse
41
-
42
- # Traverse a nested structure
43
- data = {
44
- "openapi": "3.1.0",
45
- "info": {"title": "My API", "version": "1.0.0"},
46
- "paths": {
47
- "/users": {
48
- "get": {"summary": "List users"}
49
- }
50
- }
51
- }
52
-
53
- # Walk all nodes
54
- for node in traverse(data):
55
- print(f"{node.format_path()}: {node.value}")
56
- ```
57
-
58
- Output:
59
- ```
60
- openapi: 3.1.0
61
- info: {'title': 'My API', 'version': '1.0.0'}
62
- info.title: My API
63
- info.version: 1.0.0
64
- paths: {'/users': {'get': {'summary': 'List users'}}}
65
- paths./users: {'get': {'summary': 'List users'}}
66
- paths./users.get: {'summary': 'List users'}
67
- paths./users.get.summary: List users
68
- ```
69
-
70
- ### Working with Paths
71
-
72
- ```python
73
- from jentic.apitools.openapi.traverse.json import traverse
74
-
75
- data = {
76
- "users": [
77
- {"name": "Alice", "email": "alice@example.com"},
78
- {"name": "Bob", "email": "bob@example.com"}
79
- ]
80
- }
81
-
82
- for node in traverse(data):
83
- # Access path information
84
- print(f"Path: {node.path}")
85
- print(f"Segment: {node.segment}")
86
- print(f"Full path: {node.full_path}")
87
- print(f"Formatted: {node.format_path()}")
88
- print(f"Depth: {len(node.ancestors)}")
89
- print()
90
- ```
91
-
92
- ### Custom Path Formatting
93
-
94
- ```python
95
- for node in traverse(data):
96
- # Default dot separator
97
- print(node.format_path()) # e.g., "paths./users.get.summary"
98
-
99
- # Custom separator
100
- print(node.format_path(separator="/")) # e.g., "paths//users/get/summary"
101
- ```
102
-
103
- ### Finding Specific Nodes
104
-
105
- ```python
106
- # Find all $ref references in a document
107
- refs = [
108
- node.value["$ref"]
109
- for node in traverse(openapi_doc)
110
- if isinstance(node.value, dict) and "$ref" in node.value
111
- ]
112
-
113
- # Find all nodes at a specific path segment
114
- schemas = [
115
- node.value
116
- for node in traverse(openapi_doc)
117
- if node.segment == "schema"
118
- ]
119
-
120
- # Find deeply nested values
121
- response_descriptions = [
122
- node.value
123
- for node in traverse(openapi_doc)
124
- if node.segment == "description" and "responses" in node.path
125
- ]
126
- ```
127
-
128
- ### API Reference
129
-
130
- #### `traverse(root: JSONValue) -> Iterator[TraversalNode]`
131
-
132
- Performs depth-first traversal of a JSON-like structure.
133
-
134
- **Parameters:**
135
- - `root`: The data structure to traverse (dict, list, or scalar)
136
-
137
- **Returns:**
138
- - Iterator of `TraversalNode` objects
139
-
140
- **Yields:**
141
- - For dicts: one node per key-value pair
142
- - For lists: one node per index-item pair
143
- - Scalars at root don't yield nodes (but are accessible via parent nodes)
144
-
145
- #### `TraversalNode`
146
-
147
- Immutable dataclass representing a node encountered during traversal.
148
-
149
- **Attributes:**
150
- - `path: JSONPath` - Path from root to the parent container (tuple of segments)
151
- - `parent: JSONContainer` - The parent container (dict or list)
152
- - `segment: PathSeg` - The key (for dicts) or index (for lists) within parent
153
- - `value: JSONValue` - The actual value at `parent[segment]`
154
- - `ancestors: tuple[JSONValue, ...]` - Ordered tuple of values from root down to (but not including) parent
155
-
156
- **Properties:**
157
- - `full_path: JSONPath` - Complete path from root to this value (`path + (segment,)`)
158
-
159
- **Methods:**
160
- - `format_path(separator: str = ".") -> str` - Format the full path as a human-readable string
161
-
162
- ### Usage Examples
163
-
164
- #### Collecting All Schemas
165
-
166
- ```python
167
- from jentic.apitools.openapi.traverse.json import traverse
168
-
169
- def collect_schemas(openapi_doc):
170
- """Collect all schema objects from an OpenAPI document."""
171
- schemas = []
172
-
173
- for node in traverse(openapi_doc):
174
- if node.segment == "schema" and isinstance(node.value, dict):
175
- schemas.append({
176
- "path": node.format_path(),
177
- "schema": node.value
178
- })
179
-
180
- return schemas
181
- ```
182
-
183
-
184
- #### Analyzing Document Structure
185
-
186
- ```python
187
- def analyze_depth(data):
188
- """Analyze the depth distribution of a document."""
189
- max_depth = 0
190
- depth_counts = {}
191
-
192
- for node in traverse(data):
193
- depth = len(node.ancestors)
194
- max_depth = max(max_depth, depth)
195
- depth_counts[depth] = depth_counts.get(depth, 0) + 1
196
-
197
- return {
198
- "max_depth": max_depth,
199
- "depth_distribution": depth_counts
200
- }
201
- ```
202
-
203
- ### Testing
204
-
205
- The package includes comprehensive test coverage for JSON traversal:
206
-
207
- ```bash
208
- uv run --package jentic-openapi-traverse pytest packages/jentic-openapi-traverse/tests -v
209
- ```
@@ -1,8 +0,0 @@
1
- jentic/apitools/openapi/traverse/json/__init__.py,sha256=1euUmpZviE_ECtpXYchpO8hZito2BINPjfSHMNqAU8k,326
2
- jentic/apitools/openapi/traverse/json/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- jentic/apitools/openapi/traverse/json/traversal.py,sha256=1ouszn4S29X0iJaMxxb1neyClbWXqIKwFGhHROcpBSI,3524
4
- jentic_openapi_traverse-1.0.0a22.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
5
- jentic_openapi_traverse-1.0.0a22.dist-info/licenses/NOTICE,sha256=pAOGW-rGw9KNc2cuuLWZkfx0GSTV4TicbgBKZSLPMIs,168
6
- jentic_openapi_traverse-1.0.0a22.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
7
- jentic_openapi_traverse-1.0.0a22.dist-info/METADATA,sha256=5-XpBl4fy2yjMwJVJM6Qn0qwPzfA9wZHWuKjRkKosAY,5322
8
- jentic_openapi_traverse-1.0.0a22.dist-info/RECORD,,