jentic-openapi-traverse 1.0.0a23__tar.gz → 1.0.0a25__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.
- jentic_openapi_traverse-1.0.0a23/README.md → jentic_openapi_traverse-1.0.0a25/PKG-INFO +46 -4
- jentic_openapi_traverse-1.0.0a23/PKG-INFO → jentic_openapi_traverse-1.0.0a25/README.md +31 -19
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/pyproject.toml +2 -2
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/src/jentic/apitools/openapi/traverse/datamodels/low/introspection.py +24 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/src/jentic/apitools/openapi/traverse/datamodels/low/path.py +51 -27
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/src/jentic/apitools/openapi/traverse/datamodels/low/traversal.py +13 -3
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/LICENSE +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/NOTICE +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/src/jentic/apitools/openapi/traverse/datamodels/low/__init__.py +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/src/jentic/apitools/openapi/traverse/datamodels/low/merge.py +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/src/jentic/apitools/openapi/traverse/datamodels/low/py.typed +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/src/jentic/apitools/openapi/traverse/json/__init__.py +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/src/jentic/apitools/openapi/traverse/json/py.typed +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a25}/src/jentic/apitools/openapi/traverse/json/traversal.py +0 -0
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jentic-openapi-traverse
|
|
3
|
+
Version: 1.0.0a25
|
|
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.0a25
|
|
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
|
+
|
|
1
16
|
# jentic-openapi-traverse
|
|
2
17
|
|
|
3
18
|
A Python library for traversing OpenAPI documents. This package is part of the Jentic OpenAPI Tools ecosystem and provides two types of traversal:
|
|
@@ -108,15 +123,42 @@ class PathInspector:
|
|
|
108
123
|
print(f"Parent field: {path.parent_field}") # e.g., "get", "post"
|
|
109
124
|
print(f"Parent key: {path.parent_key}") # e.g., "/users" (for path items)
|
|
110
125
|
|
|
111
|
-
# Ancestry
|
|
126
|
+
# Ancestry (computed properties)
|
|
127
|
+
print(f"Parent: {path.parent.__class__.__name__}")
|
|
112
128
|
print(f"Ancestors: {len(path.ancestors)}")
|
|
113
129
|
root = path.get_root()
|
|
114
130
|
|
|
115
|
-
#
|
|
116
|
-
print(f"JSONPointer: {path.format_path()}")
|
|
117
|
-
|
|
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']
|
|
118
137
|
```
|
|
119
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
|
+
|
|
120
162
|
### Enter/Leave Hooks
|
|
121
163
|
|
|
122
164
|
Use enter/leave hooks for pre/post processing logic:
|
|
@@ -1,18 +1,3 @@
|
|
|
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
1
|
# jentic-openapi-traverse
|
|
17
2
|
|
|
18
3
|
A Python library for traversing OpenAPI documents. This package is part of the Jentic OpenAPI Tools ecosystem and provides two types of traversal:
|
|
@@ -123,15 +108,42 @@ class PathInspector:
|
|
|
123
108
|
print(f"Parent field: {path.parent_field}") # e.g., "get", "post"
|
|
124
109
|
print(f"Parent key: {path.parent_key}") # e.g., "/users" (for path items)
|
|
125
110
|
|
|
126
|
-
# Ancestry
|
|
111
|
+
# Ancestry (computed properties)
|
|
112
|
+
print(f"Parent: {path.parent.__class__.__name__}")
|
|
127
113
|
print(f"Ancestors: {len(path.ancestors)}")
|
|
128
114
|
root = path.get_root()
|
|
129
115
|
|
|
130
|
-
#
|
|
131
|
-
print(f"JSONPointer: {path.format_path()}")
|
|
132
|
-
|
|
116
|
+
# Complete path formatting (RFC 6901 JSONPointer / RFC 9535 JSONPath)
|
|
117
|
+
print(f"JSONPointer: {path.format_path()}")
|
|
118
|
+
# Output: /paths/~1users/get
|
|
119
|
+
|
|
120
|
+
print(f"JSONPath: {path.format_path(path_format='jsonpath')}")
|
|
121
|
+
# Output: $['paths']['/users']['get']
|
|
133
122
|
```
|
|
134
123
|
|
|
124
|
+
#### Path Reconstruction
|
|
125
|
+
|
|
126
|
+
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:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
class PathFormatter:
|
|
130
|
+
def visit_Response(self, path):
|
|
131
|
+
# Complete paths from root to current node
|
|
132
|
+
pointer = path.format_path()
|
|
133
|
+
# /paths/~1users/get/responses/200
|
|
134
|
+
|
|
135
|
+
jsonpath = path.format_path(path_format='jsonpath')
|
|
136
|
+
# $['paths']['/users']['get']['responses']['200']
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Special handling for patterned fields:**
|
|
140
|
+
- Patterned fields like `Paths.paths` don't duplicate in paths: `/paths/{key}` (not `/paths/paths/{key}`)
|
|
141
|
+
- Fixed dict fields like `webhooks`, `callbacks`, `schemas` include their field name: `/webhooks/{key}`, `/components/schemas/{key}`
|
|
142
|
+
|
|
143
|
+
**Computed properties:**
|
|
144
|
+
- `path.parent` - Returns parent node (computed from parent_path chain)
|
|
145
|
+
- `path.ancestors` - Returns tuple of ancestor nodes from root to parent (computed on access)
|
|
146
|
+
|
|
135
147
|
### Enter/Leave Hooks
|
|
136
148
|
|
|
137
149
|
Use enter/leave hooks for pre/post processing logic:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "jentic-openapi-traverse"
|
|
3
|
-
version = "1.0.0-alpha.
|
|
3
|
+
version = "1.0.0-alpha.25"
|
|
4
4
|
description = "Jentic OpenAPI Traversal Utilities"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Jentic", email = "hello@jentic.com" }]
|
|
@@ -8,7 +8,7 @@ license = "Apache-2.0"
|
|
|
8
8
|
license-files = ["LICENSE", "NOTICE"]
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
11
|
-
"jentic-openapi-datamodels~=1.0.0-alpha.
|
|
11
|
+
"jentic-openapi-datamodels~=1.0.0-alpha.25",
|
|
12
12
|
"jsonpointer~=3.0.0"
|
|
13
13
|
]
|
|
14
14
|
|
|
@@ -15,9 +15,33 @@ __all__ = [
|
|
|
15
15
|
"get_traversable_fields",
|
|
16
16
|
"unwrap_value",
|
|
17
17
|
"is_datamodel_node",
|
|
18
|
+
"get_yaml_field_name",
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def get_yaml_field_name(node_class: type, python_field_name: str) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Convert Python field name to YAML field name using metadata.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
node_class: Datamodel class type
|
|
28
|
+
python_field_name: Python attribute name (e.g., "external_docs")
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
YAML field name from metadata, or python_field_name if not found
|
|
32
|
+
(e.g., "externalDocs")
|
|
33
|
+
"""
|
|
34
|
+
fixed = fixed_fields(node_class)
|
|
35
|
+
patterned = patterned_fields(node_class)
|
|
36
|
+
|
|
37
|
+
# Try fixed fields first, then patterned
|
|
38
|
+
field_obj = fixed.get(python_field_name) or patterned.get(python_field_name)
|
|
39
|
+
if field_obj:
|
|
40
|
+
return field_obj.metadata.get("yaml_name", python_field_name)
|
|
41
|
+
|
|
42
|
+
return python_field_name
|
|
43
|
+
|
|
44
|
+
|
|
21
45
|
# Cache of field names to check per class type
|
|
22
46
|
# {Info: ["title", "description", "contact", ...], Operation: [...], ...}
|
|
23
47
|
_FIELD_NAMES_CACHE: dict[type, list[str]] = {}
|
|
@@ -5,6 +5,8 @@ from typing import Any, Literal
|
|
|
5
5
|
|
|
6
6
|
from jsonpointer import JsonPointer
|
|
7
7
|
|
|
8
|
+
from .introspection import get_yaml_field_name
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
__all__ = ["NodePath"]
|
|
10
12
|
|
|
@@ -19,13 +21,28 @@ class NodePath:
|
|
|
19
21
|
"""
|
|
20
22
|
|
|
21
23
|
node: Any # Current datamodel object
|
|
22
|
-
|
|
24
|
+
parent_path: "NodePath | None" # Reference to parent's NodePath (chain)
|
|
23
25
|
parent_field: str | None # Field name in parent
|
|
24
26
|
parent_key: str | int | None # Key if parent field is list/dict
|
|
25
|
-
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def parent(self) -> Any | None:
|
|
30
|
+
"""Get parent node. Computed from parent_path for convenience."""
|
|
31
|
+
return self.parent_path.node if self.parent_path else None
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def ancestors(self) -> tuple[Any, ...]:
|
|
35
|
+
"""Get ancestor nodes from root to parent. Computed for convenience."""
|
|
36
|
+
result = []
|
|
37
|
+
current = self.parent_path
|
|
38
|
+
while current is not None:
|
|
39
|
+
result.append(current.node)
|
|
40
|
+
current = current.parent_path
|
|
41
|
+
result.reverse() # Root first
|
|
42
|
+
return tuple(result)
|
|
26
43
|
|
|
27
44
|
def create_child(
|
|
28
|
-
self, node: Any, parent_field: str, parent_key: str | int | None
|
|
45
|
+
self, node: Any, parent_field: str | None, parent_key: str | int | None
|
|
29
46
|
) -> "NodePath":
|
|
30
47
|
"""
|
|
31
48
|
Create a child NodePath from this path.
|
|
@@ -34,7 +51,7 @@ class NodePath:
|
|
|
34
51
|
|
|
35
52
|
Args:
|
|
36
53
|
node: Child node
|
|
37
|
-
parent_field: Field name in current node
|
|
54
|
+
parent_field: Field name in current node (None for dict items to avoid duplicates)
|
|
38
55
|
parent_key: Key if field is list/dict
|
|
39
56
|
|
|
40
57
|
Returns:
|
|
@@ -42,10 +59,9 @@ class NodePath:
|
|
|
42
59
|
"""
|
|
43
60
|
return NodePath(
|
|
44
61
|
node=node,
|
|
45
|
-
|
|
62
|
+
parent_path=self,
|
|
46
63
|
parent_field=parent_field,
|
|
47
64
|
parent_key=parent_key,
|
|
48
|
-
ancestors=self.ancestors + (self.node,),
|
|
49
65
|
)
|
|
50
66
|
|
|
51
67
|
def traverse(self, visitor) -> None:
|
|
@@ -103,37 +119,42 @@ class NodePath:
|
|
|
103
119
|
"$['components']['schemas']['User']['properties']['name']"
|
|
104
120
|
"""
|
|
105
121
|
# Root node
|
|
106
|
-
if
|
|
122
|
+
if self.parent_path is None:
|
|
107
123
|
return "$" if path_format == "jsonpath" else ""
|
|
108
124
|
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
# Walk back collecting all segments
|
|
126
|
+
segments: list[str | int] = []
|
|
127
|
+
current = self
|
|
128
|
+
while current.parent_path is not None:
|
|
129
|
+
# Add in reverse order (key first, then field) because we'll reverse the list
|
|
130
|
+
# This ensures field comes before key in the final path
|
|
131
|
+
if current.parent_key is not None:
|
|
132
|
+
segments.append(current.parent_key)
|
|
133
|
+
if current.parent_field:
|
|
134
|
+
# Convert Python field name to YAML name for output
|
|
135
|
+
parent_class = type(current.parent_path.node)
|
|
136
|
+
yaml_name = get_yaml_field_name(parent_class, current.parent_field)
|
|
137
|
+
segments.append(yaml_name)
|
|
138
|
+
current = current.parent_path
|
|
139
|
+
|
|
140
|
+
segments.reverse() # Root to leaf order
|
|
120
141
|
|
|
121
142
|
if path_format == "jsonpath":
|
|
122
143
|
# RFC 9535 Normalized JSONPath: $['field'][index]['key']
|
|
123
|
-
|
|
124
|
-
for
|
|
125
|
-
if isinstance(
|
|
144
|
+
result = ["$"]
|
|
145
|
+
for segment in segments:
|
|
146
|
+
if isinstance(segment, int):
|
|
126
147
|
# Array index: $[0]
|
|
127
|
-
|
|
148
|
+
result.append(f"[{segment}]")
|
|
128
149
|
else:
|
|
129
150
|
# Member name: $['field']
|
|
130
151
|
# Escape single quotes in the string
|
|
131
|
-
escaped = str(
|
|
132
|
-
|
|
133
|
-
return "".join(
|
|
152
|
+
escaped = str(segment).replace("'", "\\'")
|
|
153
|
+
result.append(f"['{escaped}']")
|
|
154
|
+
return "".join(result)
|
|
134
155
|
|
|
135
156
|
# RFC 6901 JSON Pointer
|
|
136
|
-
return JsonPointer.from_parts(
|
|
157
|
+
return JsonPointer.from_parts(segments).path
|
|
137
158
|
|
|
138
159
|
def get_root(self) -> Any:
|
|
139
160
|
"""
|
|
@@ -142,4 +163,7 @@ class NodePath:
|
|
|
142
163
|
Returns:
|
|
143
164
|
Root datamodel object
|
|
144
165
|
"""
|
|
145
|
-
|
|
166
|
+
current = self
|
|
167
|
+
while current.parent_path is not None:
|
|
168
|
+
current = current.parent_path
|
|
169
|
+
return current.node
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Core traversal functionality for low-level OpenAPI datamodels."""
|
|
2
2
|
|
|
3
|
+
from jentic.apitools.openapi.datamodels.low.fields import patterned_fields
|
|
4
|
+
|
|
3
5
|
from .introspection import get_traversable_fields, is_datamodel_node, unwrap_value
|
|
4
6
|
from .path import NodePath
|
|
5
7
|
|
|
@@ -96,10 +98,9 @@ def traverse(root, visitor) -> None:
|
|
|
96
98
|
# Create initial root path
|
|
97
99
|
initial_path = NodePath(
|
|
98
100
|
node=root,
|
|
99
|
-
|
|
101
|
+
parent_path=None,
|
|
100
102
|
parent_field=None,
|
|
101
103
|
parent_key=None,
|
|
102
|
-
ancestors=(),
|
|
103
104
|
)
|
|
104
105
|
|
|
105
106
|
# Start traversal
|
|
@@ -144,6 +145,12 @@ def _default_traverse_children(visitor, path: NodePath) -> _BreakType | None:
|
|
|
144
145
|
|
|
145
146
|
# Handle dicts
|
|
146
147
|
elif isinstance(unwrapped, dict):
|
|
148
|
+
# Check if this field is a patterned field
|
|
149
|
+
# Patterned fields (like Paths.paths, Components.schemas) should not
|
|
150
|
+
# add their field name to the path when iterating dict items
|
|
151
|
+
patterned_field_names = patterned_fields(type(path.node))
|
|
152
|
+
is_patterned = field_name in patterned_field_names
|
|
153
|
+
|
|
147
154
|
for key, value in unwrapped.items():
|
|
148
155
|
unwrapped_key = unwrap_value(key)
|
|
149
156
|
unwrapped_value = unwrap_value(value)
|
|
@@ -153,9 +160,12 @@ def _default_traverse_children(visitor, path: NodePath) -> _BreakType | None:
|
|
|
153
160
|
assert isinstance(unwrapped_key, (str, int)), (
|
|
154
161
|
f"Expected str or int key, got {type(unwrapped_key)}"
|
|
155
162
|
)
|
|
163
|
+
# For patterned fields, don't include the field name in the path
|
|
164
|
+
# (e.g., Paths.paths is patterned, so /paths/{path-key} not /paths/paths/{path-key})
|
|
165
|
+
dict_field_name: str | None = None if is_patterned else field_name
|
|
156
166
|
child_path = path.create_child(
|
|
157
167
|
node=unwrapped_value,
|
|
158
|
-
parent_field=
|
|
168
|
+
parent_field=dict_field_name,
|
|
159
169
|
parent_key=unwrapped_key,
|
|
160
170
|
)
|
|
161
171
|
result = _visit_node(visitor, child_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|