Jentic OpenAPI Traversal Utilities
Project description
jentic-openapi-traverse
A Python library for traversing OpenAPI documents. This package is part of the Jentic OpenAPI Tools ecosystem and provides two types of traversal:
- Datamodel Traversal - OpenAPI-aware semantic traversal with visitor pattern
- JSON Traversal - Generic depth-first traversal of JSON-like structures
Installation
pip install jentic-openapi-traverse
Prerequisites:
- Python 3.11+
Datamodel Traversal
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.
Quick Start
from jentic.apitools.openapi.parser.core import OpenAPIParser
from jentic.apitools.openapi.traverse.datamodels.low import traverse, DataModelLowVisitor
# Parse OpenAPI document
parser = OpenAPIParser("datamodel-low")
doc = parser.parse("file:///path/to/openapi.yaml")
# Create visitor
class OperationCollector(DataModelLowVisitor):
def __init__(self):
self.operations = []
def visit_Operation(self, path):
self.operations.append({
"path": path.format_path(path_format="jsonpointer"),
"operation_id": path.node.operation_id.value if path.node.operation_id else None
})
# Traverse
visitor = OperationCollector()
traverse(doc, visitor)
print(f"Found {len(visitor.operations)} operations")
Visitor Pattern
The datamodel traversal uses a flexible visitor pattern with multiple hook types:
Hook Methods
Generic hooks (fire for ALL nodes):
visit_enter(path)- Called before processing any nodevisit_leave(path)- Called after processing any node and its children
Class-specific hooks (fire for matching node types):
visit_ClassName(path)- Main visitor for specific node typevisit_enter_ClassName(path)- Called before visit_ClassNamevisit_leave_ClassName(path)- Called after children are visited
Dispatch Order
For each node, hooks are called in this order:
visit_enter(path)- generic entervisit_enter_ClassName(path)- specific entervisit_ClassName(path)- main visitor- [child traversal - automatic unless skipped]
visit_leave_ClassName(path)- specific leavevisit_leave(path)- generic leave
Control Flow
Visitor methods control traversal by their return value:
None(or no return) - Continue normally (children visited automatically)False- Skip children of this node (but continue to siblings)BREAK- Stop entire traversal immediately
from jentic.apitools.openapi.traverse.datamodels.low import traverse, BREAK
class ControlFlowExample:
def visit_PathItem(self, path):
if path.parent_key == "/internal":
return False # Skip internal paths and their children
def visit_Operation(self, path):
if some_error_condition:
return BREAK # Stop entire traversal
NodePath Context
Every visitor method receives a NodePath object with context about the current node:
class PathInspector:
def visit_Operation(self, path):
# Current node
print(f"Node: {path.node.__class__.__name__}")
# Parent information
print(f"Parent field: {path.parent_field}") # e.g., "get", "post"
print(f"Parent key: {path.parent_key}") # e.g., "/users" (for path items)
# Ancestry (computed properties)
print(f"Parent: {path.parent.__class__.__name__}")
print(f"Ancestors: {len(path.ancestors)}")
root = path.get_root()
# Complete path formatting (RFC 6901 JSONPointer / RFC 9535 JSONPath)
print(f"JSONPointer: {path.format_path()}")
# Output: /paths/~1users/get
print(f"JSONPath: {path.format_path(path_format='jsonpath')}")
# Output: $['paths']['/users']['get']
Path Reconstruction
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:
class PathFormatter:
def visit_Response(self, path):
# Complete paths from root to current node
pointer = path.format_path()
# /paths/~1users/get/responses/200
jsonpath = path.format_path(path_format='jsonpath')
# $['paths']['/users']['get']['responses']['200']
Special handling for patterned fields:
- Patterned fields like
Paths.pathsdon't duplicate in paths:/paths/{key}(not/paths/paths/{key}) - Fixed dict fields like
webhooks,callbacks,schemasinclude their field name:/webhooks/{key},/components/schemas/{key}
Computed properties:
path.parent- Returns parent node (computed from parent_path chain)path.ancestors- Returns tuple of ancestor nodes from root to parent (computed on access)
Enter/Leave Hooks
Use enter/leave hooks for pre/post processing logic:
class DepthTracker(DataModelLowVisitor):
def __init__(self):
self.current_depth = 0
self.max_depth = 0
def visit_enter(self, path):
self.current_depth += 1
self.max_depth = max(self.max_depth, self.current_depth)
print(" " * self.current_depth + f"Entering {path.node.__class__.__name__}")
def visit_leave(self, path):
print(" " * self.current_depth + f"Leaving {path.node.__class__.__name__}")
self.current_depth -= 1
Examples
Collecting All Schemas
class SchemaCollector(DataModelLowVisitor):
def __init__(self):
self.schemas = {}
def visit_Schema(self, path):
schema_name = path.parent_key if path.parent_field == "schemas" else None
if schema_name:
self.schemas[schema_name] = path.node
visitor = SchemaCollector()
traverse(doc, visitor)
print(f"Found {len(visitor.schemas)} schemas")
Validating Security Requirements
class SecurityValidator(DataModelLowVisitor):
def __init__(self):
self.errors = []
def visit_Operation(self, path):
if not path.node.security:
self.errors.append(f"Missing security at {path.format_path()}")
def visit_SecurityRequirement(self, path):
# Validate security requirement
if not path.node.schemes:
self.errors.append(f"Empty security requirement at {path.format_path()}")
visitor = SecurityValidator()
traverse(doc, visitor)
if visitor.errors:
for error in visitor.errors:
print(f"Security error: {error}")
Finding Deprecated Operations
class DeprecatedFinder:
def __init__(self):
self.deprecated_ops = []
def visit_Operation(self, path):
if path.node.deprecated and path.node.deprecated.value:
self.deprecated_ops.append({
"path": path.format_path(),
"operation_id": path.node.operation_id.value if path.node.operation_id else None,
"method": path.parent_field
})
return False # Skip children (we don't need to go deeper)
visitor = DeprecatedFinder()
traverse(doc, visitor)
Early Exit on Error
class ErrorDetector(DataModelLowVisitor):
def __init__(self):
self.error_found = False
self.error_location = None
def visit_Operation(self, path):
if not path.node.responses:
self.error_found = True
self.error_location = path.format_path()
return BREAK # Stop traversal immediately
Merging Multiple Visitors
Run multiple visitors in a single traversal pass (parallel visitation) using merge_visitors:
from jentic.apitools.openapi.traverse.datamodels.low import merge_visitors
# Create separate visitors
schema_collector = SchemaCollector()
security_validator = SecurityValidator()
deprecated_finder = DeprecatedFinder()
# Merge and traverse once
merged = merge_visitors(schema_collector, security_validator, deprecated_finder)
traverse(doc, merged)
# Each visitor maintains independent state
print(f"Schemas: {len(schema_collector.schemas)}")
print(f"Security errors: {len(security_validator.errors)}")
print(f"Deprecated: {len(deprecated_finder.deprecated_ops)}")
Per-Visitor Control Flow:
- Each visitor can independently skip subtrees or break
- If
visitor1returnsFalse, onlyvisitor1skips children - Other visitors continue normally
- This follows ApiDOM's per-visitor semantics
Duck Typing
You don't need to inherit from DataModelLowVisitor - duck typing works:
class SimpleCounter: # No inheritance
def __init__(self):
self.count = 0
def visit_Operation(self, path):
self.count += 1
visitor = SimpleCounter()
traverse(doc, visitor)
The DataModelLowVisitor base class is optional and provides no functionality - it's purely for organizational purposes.
API Reference
traverse(root, visitor) -> None
Traverse OpenAPI datamodel tree using visitor pattern.
Parameters:
root- Root datamodel object (OpenAPI30, OpenAPI31, or any datamodel node)visitor- Object withvisit_*methods (duck typing)
Returns:
- None (traversal is side-effect based)
BREAK
Sentinel value to stop traversal immediately. Return this from any visitor method.
from jentic.apitools.openapi.traverse.datamodels.low import BREAK
def visit_Operation(self, path):
if should_stop:
return BREAK
merge_visitors(*visitors) -> object
Merge multiple visitors into one composite visitor.
Parameters:
*visitors- Variable number of visitor objects
Returns:
- Composite visitor object with per-visitor state tracking
JSON Traversal
Generic depth-first traversal of any JSON-like structure (dicts, lists, scalars). Works with raw parsed OpenAPI documents or any other JSON data.
Quick Start
from jentic.apitools.openapi.traverse.json import traverse
# Traverse a nested structure
data = {
"openapi": "3.1.0",
"info": {"title": "My API", "version": "1.0.0"},
"paths": {
"/users": {
"get": {"summary": "List users"}
}
}
}
# Walk all nodes
for node in traverse(data):
print(f"{node.format_path()}: {node.value}")
Output:
openapi: 3.1.0
info: {'title': 'My API', 'version': '1.0.0'}
info.title: My API
info.version: 1.0.0
paths: {'/users': {'get': {'summary': 'List users'}}}
paths./users: {'get': {'summary': 'List users'}}
paths./users.get: {'summary': 'List users'}
paths./users.get.summary: List users
Working with Paths
from jentic.apitools.openapi.traverse.json import traverse
data = {
"users": [
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"}
]
}
for node in traverse(data):
# Access path information
print(f"Path: {node.path}")
print(f"Segment: {node.segment}")
print(f"Full path: {node.full_path}")
print(f"Formatted: {node.format_path()}")
print(f"Depth: {len(node.ancestors)}")
print()
Custom Path Formatting
for node in traverse(data):
# Default dot separator
print(node.format_path()) # e.g., "paths./users.get.summary"
# Custom separator
print(node.format_path(separator="/")) # e.g., "paths//users/get/summary"
Finding Specific Nodes
# Find all $ref references in a document
refs = [
node.value["$ref"]
for node in traverse(openapi_doc)
if isinstance(node.value, dict) and "$ref" in node.value
]
# Find all nodes at a specific path segment
schemas = [
node.value
for node in traverse(openapi_doc)
if node.segment == "schema"
]
# Find deeply nested values
response_descriptions = [
node.value
for node in traverse(openapi_doc)
if node.segment == "description" and "responses" in node.path
]
API Reference
traverse(root: JSONValue) -> Iterator[TraversalNode]
Performs depth-first traversal of a JSON-like structure.
Parameters:
root: The data structure to traverse (dict, list, or scalar)
Returns:
- Iterator of
TraversalNodeobjects
Yields:
- For dicts: one node per key-value pair
- For lists: one node per index-item pair
- Scalars at root don't yield nodes (but are accessible via parent nodes)
TraversalNode
Immutable dataclass representing a node encountered during traversal.
Attributes:
path: JSONPath- Path from root to the parent container (tuple of segments)parent: JSONContainer- The parent container (dict or list)segment: PathSeg- The key (for dicts) or index (for lists) within parentvalue: JSONValue- The actual value atparent[segment]ancestors: tuple[JSONValue, ...]- Ordered tuple of values from root down to (but not including) parent
Properties:
full_path: JSONPath- Complete path from root to this value (path + (segment,))
Methods:
format_path(separator: str = ".") -> str- Format the full path as a human-readable string
Usage Examples
Collecting All Schemas
from jentic.apitools.openapi.traverse.json import traverse
def collect_schemas(openapi_doc):
"""Collect all schema objects from an OpenAPI document."""
schemas = []
for node in traverse(openapi_doc):
if node.segment == "schema" and isinstance(node.value, dict):
schemas.append({
"path": node.format_path(),
"schema": node.value
})
return schemas
Analyzing Document Structure
def analyze_depth(data):
"""Analyze the depth distribution of a document."""
max_depth = 0
depth_counts = {}
for node in traverse(data):
depth = len(node.ancestors)
max_depth = max(max_depth, depth)
depth_counts[depth] = depth_counts.get(depth, 0) + 1
return {
"max_depth": max_depth,
"depth_distribution": depth_counts
}
Testing
The package includes comprehensive test coverage for JSON traversal:
uv run --package jentic-openapi-traverse pytest packages/jentic-openapi-traverse/tests -v
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jentic_openapi_traverse-1.0.0a25.tar.gz.
File metadata
- Download URL: jentic_openapi_traverse-1.0.0a25.tar.gz
- Upload date:
- Size: 16.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
174473c1ac1dc56b1da462dfc954a00a5412bf6d36e07f1116cb8a19624ddd6c
|
|
| MD5 |
b2d6ecc1585513fe3e6ba83109454846
|
|
| BLAKE2b-256 |
ef388dc91880e5ae9fef1759e176e7598d6f9d641021b263e5b0a75532fbae6f
|
Provenance
The following attestation bundles were made for jentic_openapi_traverse-1.0.0a25.tar.gz:
Publisher:
release.yml on jentic/jentic-openapi-tools
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jentic_openapi_traverse-1.0.0a25.tar.gz -
Subject digest:
174473c1ac1dc56b1da462dfc954a00a5412bf6d36e07f1116cb8a19624ddd6c - Sigstore transparency entry: 725184534
- Sigstore integration time:
-
Permalink:
jentic/jentic-openapi-tools@a26724111c491f143f7d6ade1d6192476007307b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jentic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a26724111c491f143f7d6ade1d6192476007307b -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file jentic_openapi_traverse-1.0.0a25-py3-none-any.whl.
File metadata
- Download URL: jentic_openapi_traverse-1.0.0a25-py3-none-any.whl
- Upload date:
- Size: 21.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d204a4b7835b46510f4e5be9e1c6b46ba2b0a1f1bff8e4aaacac2d25fc3e1d76
|
|
| MD5 |
a76636acbac9268981f0cea8628714e0
|
|
| BLAKE2b-256 |
9e28148dcb4bcc44102164539119c8234a3186fa5cc308d71143c2f4d429808b
|
Provenance
The following attestation bundles were made for jentic_openapi_traverse-1.0.0a25-py3-none-any.whl:
Publisher:
release.yml on jentic/jentic-openapi-tools
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jentic_openapi_traverse-1.0.0a25-py3-none-any.whl -
Subject digest:
d204a4b7835b46510f4e5be9e1c6b46ba2b0a1f1bff8e4aaacac2d25fc3e1d76 - Sigstore transparency entry: 725184557
- Sigstore integration time:
-
Permalink:
jentic/jentic-openapi-tools@a26724111c491f143f7d6ade1d6192476007307b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jentic
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a26724111c491f143f7d6ade1d6192476007307b -
Trigger Event:
workflow_dispatch
-
Statement type: