jentic-openapi-traverse 1.0.0a22__tar.gz → 1.0.0a24__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 (16) hide show
  1. jentic_openapi_traverse-1.0.0a24/PKG-INFO +518 -0
  2. jentic_openapi_traverse-1.0.0a24/README.md +503 -0
  3. {jentic_openapi_traverse-1.0.0a22 → jentic_openapi_traverse-1.0.0a24}/pyproject.toml +14 -2
  4. jentic_openapi_traverse-1.0.0a24/src/jentic/apitools/openapi/traverse/datamodels/low/__init__.py +14 -0
  5. jentic_openapi_traverse-1.0.0a24/src/jentic/apitools/openapi/traverse/datamodels/low/introspection.py +114 -0
  6. jentic_openapi_traverse-1.0.0a24/src/jentic/apitools/openapi/traverse/datamodels/low/merge.py +111 -0
  7. jentic_openapi_traverse-1.0.0a24/src/jentic/apitools/openapi/traverse/datamodels/low/path.py +164 -0
  8. jentic_openapi_traverse-1.0.0a24/src/jentic/apitools/openapi/traverse/datamodels/low/traversal.py +245 -0
  9. jentic_openapi_traverse-1.0.0a24/src/jentic/apitools/openapi/traverse/json/py.typed +0 -0
  10. jentic_openapi_traverse-1.0.0a22/PKG-INFO +0 -209
  11. jentic_openapi_traverse-1.0.0a22/README.md +0 -196
  12. {jentic_openapi_traverse-1.0.0a22 → jentic_openapi_traverse-1.0.0a24}/LICENSE +0 -0
  13. {jentic_openapi_traverse-1.0.0a22 → jentic_openapi_traverse-1.0.0a24}/NOTICE +0 -0
  14. {jentic_openapi_traverse-1.0.0a22/src/jentic/apitools/openapi/traverse/json → jentic_openapi_traverse-1.0.0a24/src/jentic/apitools/openapi/traverse/datamodels/low}/py.typed +0 -0
  15. {jentic_openapi_traverse-1.0.0a22 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/json/__init__.py +0 -0
  16. {jentic_openapi_traverse-1.0.0a22 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/json/traversal.py +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
+ ```