jentic-openapi-traverse 1.0.0a22__py3-none-any.whl → 1.0.0a24__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.
- jentic/apitools/openapi/traverse/datamodels/low/__init__.py +14 -0
- jentic/apitools/openapi/traverse/datamodels/low/introspection.py +114 -0
- jentic/apitools/openapi/traverse/datamodels/low/merge.py +111 -0
- jentic/apitools/openapi/traverse/datamodels/low/path.py +164 -0
- jentic/apitools/openapi/traverse/datamodels/low/py.typed +0 -0
- jentic/apitools/openapi/traverse/datamodels/low/traversal.py +245 -0
- jentic_openapi_traverse-1.0.0a24.dist-info/METADATA +518 -0
- jentic_openapi_traverse-1.0.0a24.dist-info/RECORD +14 -0
- jentic_openapi_traverse-1.0.0a22.dist-info/METADATA +0 -209
- jentic_openapi_traverse-1.0.0a22.dist-info/RECORD +0 -8
- {jentic_openapi_traverse-1.0.0a22.dist-info → jentic_openapi_traverse-1.0.0a24.dist-info}/WHEEL +0 -0
- {jentic_openapi_traverse-1.0.0a22.dist-info → jentic_openapi_traverse-1.0.0a24.dist-info}/licenses/LICENSE +0 -0
- {jentic_openapi_traverse-1.0.0a22.dist-info → jentic_openapi_traverse-1.0.0a24.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jentic-openapi-traverse
|
|
3
|
+
Version: 1.0.0a24
|
|
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.0a24
|
|
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 (computed properties)
|
|
127
|
+
print(f"Parent: {path.parent.__class__.__name__}")
|
|
128
|
+
print(f"Ancestors: {len(path.ancestors)}")
|
|
129
|
+
root = path.get_root()
|
|
130
|
+
|
|
131
|
+
# Complete path formatting (RFC 6901 JSONPointer / RFC 9535 JSONPath)
|
|
132
|
+
print(f"JSONPointer: {path.format_path()}")
|
|
133
|
+
# Output: /paths/~1users/get
|
|
134
|
+
|
|
135
|
+
print(f"JSONPath: {path.format_path(path_format='jsonpath')}")
|
|
136
|
+
# Output: $['paths']['/users']['get']
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Path Reconstruction
|
|
140
|
+
|
|
141
|
+
NodePath uses a linked chain structure (`parent_path`) internally to preserve complete path information from root to current node. This enables accurate JSONPointer and JSONPath reconstruction:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
class PathFormatter:
|
|
145
|
+
def visit_Response(self, path):
|
|
146
|
+
# Complete paths from root to current node
|
|
147
|
+
pointer = path.format_path()
|
|
148
|
+
# /paths/~1users/get/responses/200
|
|
149
|
+
|
|
150
|
+
jsonpath = path.format_path(path_format='jsonpath')
|
|
151
|
+
# $['paths']['/users']['get']['responses']['200']
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Special handling for patterned fields:**
|
|
155
|
+
- Patterned fields like `Paths.paths` don't duplicate in paths: `/paths/{key}` (not `/paths/paths/{key}`)
|
|
156
|
+
- Fixed dict fields like `webhooks`, `callbacks`, `schemas` include their field name: `/webhooks/{key}`, `/components/schemas/{key}`
|
|
157
|
+
|
|
158
|
+
**Computed properties:**
|
|
159
|
+
- `path.parent` - Returns parent node (computed from parent_path chain)
|
|
160
|
+
- `path.ancestors` - Returns tuple of ancestor nodes from root to parent (computed on access)
|
|
161
|
+
|
|
162
|
+
### Enter/Leave Hooks
|
|
163
|
+
|
|
164
|
+
Use enter/leave hooks for pre/post processing logic:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
class DepthTracker(DataModelLowVisitor):
|
|
168
|
+
def __init__(self):
|
|
169
|
+
self.current_depth = 0
|
|
170
|
+
self.max_depth = 0
|
|
171
|
+
|
|
172
|
+
def visit_enter(self, path):
|
|
173
|
+
self.current_depth += 1
|
|
174
|
+
self.max_depth = max(self.max_depth, self.current_depth)
|
|
175
|
+
print(" " * self.current_depth + f"Entering {path.node.__class__.__name__}")
|
|
176
|
+
|
|
177
|
+
def visit_leave(self, path):
|
|
178
|
+
print(" " * self.current_depth + f"Leaving {path.node.__class__.__name__}")
|
|
179
|
+
self.current_depth -= 1
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Examples
|
|
183
|
+
|
|
184
|
+
#### Collecting All Schemas
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
class SchemaCollector(DataModelLowVisitor):
|
|
188
|
+
def __init__(self):
|
|
189
|
+
self.schemas = {}
|
|
190
|
+
|
|
191
|
+
def visit_Schema(self, path):
|
|
192
|
+
schema_name = path.parent_key if path.parent_field == "schemas" else None
|
|
193
|
+
if schema_name:
|
|
194
|
+
self.schemas[schema_name] = path.node
|
|
195
|
+
|
|
196
|
+
visitor = SchemaCollector()
|
|
197
|
+
traverse(doc, visitor)
|
|
198
|
+
print(f"Found {len(visitor.schemas)} schemas")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Validating Security Requirements
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
class SecurityValidator(DataModelLowVisitor):
|
|
205
|
+
def __init__(self):
|
|
206
|
+
self.errors = []
|
|
207
|
+
|
|
208
|
+
def visit_Operation(self, path):
|
|
209
|
+
if not path.node.security:
|
|
210
|
+
self.errors.append(f"Missing security at {path.format_path()}")
|
|
211
|
+
|
|
212
|
+
def visit_SecurityRequirement(self, path):
|
|
213
|
+
# Validate security requirement
|
|
214
|
+
if not path.node.schemes:
|
|
215
|
+
self.errors.append(f"Empty security requirement at {path.format_path()}")
|
|
216
|
+
|
|
217
|
+
visitor = SecurityValidator()
|
|
218
|
+
traverse(doc, visitor)
|
|
219
|
+
if visitor.errors:
|
|
220
|
+
for error in visitor.errors:
|
|
221
|
+
print(f"Security error: {error}")
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Finding Deprecated Operations
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
class DeprecatedFinder:
|
|
228
|
+
def __init__(self):
|
|
229
|
+
self.deprecated_ops = []
|
|
230
|
+
|
|
231
|
+
def visit_Operation(self, path):
|
|
232
|
+
if path.node.deprecated and path.node.deprecated.value:
|
|
233
|
+
self.deprecated_ops.append({
|
|
234
|
+
"path": path.format_path(),
|
|
235
|
+
"operation_id": path.node.operation_id.value if path.node.operation_id else None,
|
|
236
|
+
"method": path.parent_field
|
|
237
|
+
})
|
|
238
|
+
return False # Skip children (we don't need to go deeper)
|
|
239
|
+
|
|
240
|
+
visitor = DeprecatedFinder()
|
|
241
|
+
traverse(doc, visitor)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Early Exit on Error
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
class ErrorDetector(DataModelLowVisitor):
|
|
248
|
+
def __init__(self):
|
|
249
|
+
self.error_found = False
|
|
250
|
+
self.error_location = None
|
|
251
|
+
|
|
252
|
+
def visit_Operation(self, path):
|
|
253
|
+
if not path.node.responses:
|
|
254
|
+
self.error_found = True
|
|
255
|
+
self.error_location = path.format_path()
|
|
256
|
+
return BREAK # Stop traversal immediately
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Merging Multiple Visitors
|
|
260
|
+
|
|
261
|
+
Run multiple visitors in a single traversal pass (parallel visitation) using `merge_visitors`:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from jentic.apitools.openapi.traverse.datamodels.low import merge_visitors
|
|
265
|
+
|
|
266
|
+
# Create separate visitors
|
|
267
|
+
schema_collector = SchemaCollector()
|
|
268
|
+
security_validator = SecurityValidator()
|
|
269
|
+
deprecated_finder = DeprecatedFinder()
|
|
270
|
+
|
|
271
|
+
# Merge and traverse once
|
|
272
|
+
merged = merge_visitors(schema_collector, security_validator, deprecated_finder)
|
|
273
|
+
traverse(doc, merged)
|
|
274
|
+
|
|
275
|
+
# Each visitor maintains independent state
|
|
276
|
+
print(f"Schemas: {len(schema_collector.schemas)}")
|
|
277
|
+
print(f"Security errors: {len(security_validator.errors)}")
|
|
278
|
+
print(f"Deprecated: {len(deprecated_finder.deprecated_ops)}")
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Per-Visitor Control Flow:**
|
|
282
|
+
- Each visitor can independently skip subtrees or break
|
|
283
|
+
- If `visitor1` returns `False`, only `visitor1` skips children
|
|
284
|
+
- Other visitors continue normally
|
|
285
|
+
- This follows ApiDOM's per-visitor semantics
|
|
286
|
+
|
|
287
|
+
### Duck Typing
|
|
288
|
+
|
|
289
|
+
You don't need to inherit from `DataModelLowVisitor` - duck typing works:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
class SimpleCounter: # No inheritance
|
|
293
|
+
def __init__(self):
|
|
294
|
+
self.count = 0
|
|
295
|
+
|
|
296
|
+
def visit_Operation(self, path):
|
|
297
|
+
self.count += 1
|
|
298
|
+
|
|
299
|
+
visitor = SimpleCounter()
|
|
300
|
+
traverse(doc, visitor)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The `DataModelLowVisitor` base class is optional and provides no functionality - it's purely for organizational purposes.
|
|
304
|
+
|
|
305
|
+
### API Reference
|
|
306
|
+
|
|
307
|
+
#### `traverse(root, visitor) -> None`
|
|
308
|
+
|
|
309
|
+
Traverse OpenAPI datamodel tree using visitor pattern.
|
|
310
|
+
|
|
311
|
+
**Parameters:**
|
|
312
|
+
- `root` - Root datamodel object (OpenAPI30, OpenAPI31, or any datamodel node)
|
|
313
|
+
- `visitor` - Object with `visit_*` methods (duck typing)
|
|
314
|
+
|
|
315
|
+
**Returns:**
|
|
316
|
+
- None (traversal is side-effect based)
|
|
317
|
+
|
|
318
|
+
#### `BREAK`
|
|
319
|
+
|
|
320
|
+
Sentinel value to stop traversal immediately. Return this from any visitor method.
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from jentic.apitools.openapi.traverse.datamodels.low import BREAK
|
|
324
|
+
|
|
325
|
+
def visit_Operation(self, path):
|
|
326
|
+
if should_stop:
|
|
327
|
+
return BREAK
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### `merge_visitors(*visitors) -> object`
|
|
331
|
+
|
|
332
|
+
Merge multiple visitors into one composite visitor.
|
|
333
|
+
|
|
334
|
+
**Parameters:**
|
|
335
|
+
- `*visitors` - Variable number of visitor objects
|
|
336
|
+
|
|
337
|
+
**Returns:**
|
|
338
|
+
- Composite visitor object with per-visitor state tracking
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
## JSON Traversal
|
|
342
|
+
|
|
343
|
+
Generic depth-first traversal of any JSON-like structure (dicts, lists, scalars).
|
|
344
|
+
Works with raw parsed OpenAPI documents or any other JSON data.
|
|
345
|
+
|
|
346
|
+
### Quick Start
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
from jentic.apitools.openapi.traverse.json import traverse
|
|
350
|
+
|
|
351
|
+
# Traverse a nested structure
|
|
352
|
+
data = {
|
|
353
|
+
"openapi": "3.1.0",
|
|
354
|
+
"info": {"title": "My API", "version": "1.0.0"},
|
|
355
|
+
"paths": {
|
|
356
|
+
"/users": {
|
|
357
|
+
"get": {"summary": "List users"}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# Walk all nodes
|
|
363
|
+
for node in traverse(data):
|
|
364
|
+
print(f"{node.format_path()}: {node.value}")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Output:
|
|
368
|
+
```
|
|
369
|
+
openapi: 3.1.0
|
|
370
|
+
info: {'title': 'My API', 'version': '1.0.0'}
|
|
371
|
+
info.title: My API
|
|
372
|
+
info.version: 1.0.0
|
|
373
|
+
paths: {'/users': {'get': {'summary': 'List users'}}}
|
|
374
|
+
paths./users: {'get': {'summary': 'List users'}}
|
|
375
|
+
paths./users.get: {'summary': 'List users'}
|
|
376
|
+
paths./users.get.summary: List users
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Working with Paths
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
from jentic.apitools.openapi.traverse.json import traverse
|
|
383
|
+
|
|
384
|
+
data = {
|
|
385
|
+
"users": [
|
|
386
|
+
{"name": "Alice", "email": "alice@example.com"},
|
|
387
|
+
{"name": "Bob", "email": "bob@example.com"}
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for node in traverse(data):
|
|
392
|
+
# Access path information
|
|
393
|
+
print(f"Path: {node.path}")
|
|
394
|
+
print(f"Segment: {node.segment}")
|
|
395
|
+
print(f"Full path: {node.full_path}")
|
|
396
|
+
print(f"Formatted: {node.format_path()}")
|
|
397
|
+
print(f"Depth: {len(node.ancestors)}")
|
|
398
|
+
print()
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Custom Path Formatting
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
for node in traverse(data):
|
|
405
|
+
# Default dot separator
|
|
406
|
+
print(node.format_path()) # e.g., "paths./users.get.summary"
|
|
407
|
+
|
|
408
|
+
# Custom separator
|
|
409
|
+
print(node.format_path(separator="/")) # e.g., "paths//users/get/summary"
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Finding Specific Nodes
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
# Find all $ref references in a document
|
|
416
|
+
refs = [
|
|
417
|
+
node.value["$ref"]
|
|
418
|
+
for node in traverse(openapi_doc)
|
|
419
|
+
if isinstance(node.value, dict) and "$ref" in node.value
|
|
420
|
+
]
|
|
421
|
+
|
|
422
|
+
# Find all nodes at a specific path segment
|
|
423
|
+
schemas = [
|
|
424
|
+
node.value
|
|
425
|
+
for node in traverse(openapi_doc)
|
|
426
|
+
if node.segment == "schema"
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
# Find deeply nested values
|
|
430
|
+
response_descriptions = [
|
|
431
|
+
node.value
|
|
432
|
+
for node in traverse(openapi_doc)
|
|
433
|
+
if node.segment == "description" and "responses" in node.path
|
|
434
|
+
]
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### API Reference
|
|
438
|
+
|
|
439
|
+
#### `traverse(root: JSONValue) -> Iterator[TraversalNode]`
|
|
440
|
+
|
|
441
|
+
Performs depth-first traversal of a JSON-like structure.
|
|
442
|
+
|
|
443
|
+
**Parameters:**
|
|
444
|
+
- `root`: The data structure to traverse (dict, list, or scalar)
|
|
445
|
+
|
|
446
|
+
**Returns:**
|
|
447
|
+
- Iterator of `TraversalNode` objects
|
|
448
|
+
|
|
449
|
+
**Yields:**
|
|
450
|
+
- For dicts: one node per key-value pair
|
|
451
|
+
- For lists: one node per index-item pair
|
|
452
|
+
- Scalars at root don't yield nodes (but are accessible via parent nodes)
|
|
453
|
+
|
|
454
|
+
#### `TraversalNode`
|
|
455
|
+
|
|
456
|
+
Immutable dataclass representing a node encountered during traversal.
|
|
457
|
+
|
|
458
|
+
**Attributes:**
|
|
459
|
+
- `path: JSONPath` - Path from root to the parent container (tuple of segments)
|
|
460
|
+
- `parent: JSONContainer` - The parent container (dict or list)
|
|
461
|
+
- `segment: PathSeg` - The key (for dicts) or index (for lists) within parent
|
|
462
|
+
- `value: JSONValue` - The actual value at `parent[segment]`
|
|
463
|
+
- `ancestors: tuple[JSONValue, ...]` - Ordered tuple of values from root down to (but not including) parent
|
|
464
|
+
|
|
465
|
+
**Properties:**
|
|
466
|
+
- `full_path: JSONPath` - Complete path from root to this value (`path + (segment,)`)
|
|
467
|
+
|
|
468
|
+
**Methods:**
|
|
469
|
+
- `format_path(separator: str = ".") -> str` - Format the full path as a human-readable string
|
|
470
|
+
|
|
471
|
+
### Usage Examples
|
|
472
|
+
|
|
473
|
+
#### Collecting All Schemas
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
from jentic.apitools.openapi.traverse.json import traverse
|
|
477
|
+
|
|
478
|
+
def collect_schemas(openapi_doc):
|
|
479
|
+
"""Collect all schema objects from an OpenAPI document."""
|
|
480
|
+
schemas = []
|
|
481
|
+
|
|
482
|
+
for node in traverse(openapi_doc):
|
|
483
|
+
if node.segment == "schema" and isinstance(node.value, dict):
|
|
484
|
+
schemas.append({
|
|
485
|
+
"path": node.format_path(),
|
|
486
|
+
"schema": node.value
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
return schemas
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
#### Analyzing Document Structure
|
|
494
|
+
|
|
495
|
+
```python
|
|
496
|
+
def analyze_depth(data):
|
|
497
|
+
"""Analyze the depth distribution of a document."""
|
|
498
|
+
max_depth = 0
|
|
499
|
+
depth_counts = {}
|
|
500
|
+
|
|
501
|
+
for node in traverse(data):
|
|
502
|
+
depth = len(node.ancestors)
|
|
503
|
+
max_depth = max(max_depth, depth)
|
|
504
|
+
depth_counts[depth] = depth_counts.get(depth, 0) + 1
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
"max_depth": max_depth,
|
|
508
|
+
"depth_distribution": depth_counts
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Testing
|
|
513
|
+
|
|
514
|
+
The package includes comprehensive test coverage for JSON traversal:
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
uv run --package jentic-openapi-traverse pytest packages/jentic-openapi-traverse/tests -v
|
|
518
|
+
```
|
|
@@ -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=l-2w0uV4OobkswNfhBXkv9wgGMYyB4HEpPTivMmto8o,5442
|
|
5
|
+
jentic/apitools/openapi/traverse/datamodels/low/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
jentic/apitools/openapi/traverse/datamodels/low/traversal.py,sha256=cAWnTQjmb5fKafbiEaJvv-FK-3fuip-EhdOLfZLVQjk,8774
|
|
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.0a24.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
11
|
+
jentic_openapi_traverse-1.0.0a24.dist-info/licenses/NOTICE,sha256=pAOGW-rGw9KNc2cuuLWZkfx0GSTV4TicbgBKZSLPMIs,168
|
|
12
|
+
jentic_openapi_traverse-1.0.0a24.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
13
|
+
jentic_openapi_traverse-1.0.0a24.dist-info/METADATA,sha256=m7DrnZ_zS1W1NHvPLFxAIx4_dLZxGAFrApk0mG5cEsg,14733
|
|
14
|
+
jentic_openapi_traverse-1.0.0a24.dist-info/RECORD,,
|