jentic-openapi-traverse 1.0.0a23__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.
- jentic_openapi_traverse-1.0.0a23/README.md → jentic_openapi_traverse-1.0.0a24/PKG-INFO +46 -4
- jentic_openapi_traverse-1.0.0a23/PKG-INFO → jentic_openapi_traverse-1.0.0a24/README.md +31 -19
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/pyproject.toml +2 -2
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/datamodels/low/path.py +45 -26
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/datamodels/low/traversal.py +13 -3
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/LICENSE +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/NOTICE +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/datamodels/low/__init__.py +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/datamodels/low/introspection.py +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/datamodels/low/merge.py +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/datamodels/low/py.typed +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/json/__init__.py +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/src/jentic/apitools/openapi/traverse/json/py.typed +0 -0
- {jentic_openapi_traverse-1.0.0a23 → jentic_openapi_traverse-1.0.0a24}/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.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
|
+
|
|
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.24"
|
|
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.24",
|
|
12
12
|
"jsonpointer~=3.0.0"
|
|
13
13
|
]
|
|
14
14
|
|
|
@@ -19,13 +19,28 @@ class NodePath:
|
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
node: Any # Current datamodel object
|
|
22
|
-
|
|
22
|
+
parent_path: "NodePath | None" # Reference to parent's NodePath (chain)
|
|
23
23
|
parent_field: str | None # Field name in parent
|
|
24
24
|
parent_key: str | int | None # Key if parent field is list/dict
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def parent(self) -> Any | None:
|
|
28
|
+
"""Get parent node. Computed from parent_path for convenience."""
|
|
29
|
+
return self.parent_path.node if self.parent_path else None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def ancestors(self) -> tuple[Any, ...]:
|
|
33
|
+
"""Get ancestor nodes from root to parent. Computed for convenience."""
|
|
34
|
+
result = []
|
|
35
|
+
current = self.parent_path
|
|
36
|
+
while current is not None:
|
|
37
|
+
result.append(current.node)
|
|
38
|
+
current = current.parent_path
|
|
39
|
+
result.reverse() # Root first
|
|
40
|
+
return tuple(result)
|
|
26
41
|
|
|
27
42
|
def create_child(
|
|
28
|
-
self, node: Any, parent_field: str, parent_key: str | int | None
|
|
43
|
+
self, node: Any, parent_field: str | None, parent_key: str | int | None
|
|
29
44
|
) -> "NodePath":
|
|
30
45
|
"""
|
|
31
46
|
Create a child NodePath from this path.
|
|
@@ -34,7 +49,7 @@ class NodePath:
|
|
|
34
49
|
|
|
35
50
|
Args:
|
|
36
51
|
node: Child node
|
|
37
|
-
parent_field: Field name in current node
|
|
52
|
+
parent_field: Field name in current node (None for dict items to avoid duplicates)
|
|
38
53
|
parent_key: Key if field is list/dict
|
|
39
54
|
|
|
40
55
|
Returns:
|
|
@@ -42,10 +57,9 @@ class NodePath:
|
|
|
42
57
|
"""
|
|
43
58
|
return NodePath(
|
|
44
59
|
node=node,
|
|
45
|
-
|
|
60
|
+
parent_path=self,
|
|
46
61
|
parent_field=parent_field,
|
|
47
62
|
parent_key=parent_key,
|
|
48
|
-
ancestors=self.ancestors + (self.node,),
|
|
49
63
|
)
|
|
50
64
|
|
|
51
65
|
def traverse(self, visitor) -> None:
|
|
@@ -103,37 +117,39 @@ class NodePath:
|
|
|
103
117
|
"$['components']['schemas']['User']['properties']['name']"
|
|
104
118
|
"""
|
|
105
119
|
# Root node
|
|
106
|
-
if
|
|
120
|
+
if self.parent_path is None:
|
|
107
121
|
return "$" if path_format == "jsonpath" else ""
|
|
108
122
|
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
# Walk back collecting all segments
|
|
124
|
+
segments: list[str | int] = []
|
|
125
|
+
current = self
|
|
126
|
+
while current.parent_path is not None:
|
|
127
|
+
# Add in reverse order (key first, then field) because we'll reverse the list
|
|
128
|
+
# This ensures field comes before key in the final path
|
|
129
|
+
if current.parent_key is not None:
|
|
130
|
+
segments.append(current.parent_key)
|
|
131
|
+
if current.parent_field:
|
|
132
|
+
segments.append(current.parent_field)
|
|
133
|
+
current = current.parent_path
|
|
116
134
|
|
|
117
|
-
|
|
118
|
-
# Both int (array index) and str (object key) work with from_parts
|
|
119
|
-
parts.append(self.parent_key)
|
|
135
|
+
segments.reverse() # Root to leaf order
|
|
120
136
|
|
|
121
137
|
if path_format == "jsonpath":
|
|
122
138
|
# RFC 9535 Normalized JSONPath: $['field'][index]['key']
|
|
123
|
-
|
|
124
|
-
for
|
|
125
|
-
if isinstance(
|
|
139
|
+
result = ["$"]
|
|
140
|
+
for segment in segments:
|
|
141
|
+
if isinstance(segment, int):
|
|
126
142
|
# Array index: $[0]
|
|
127
|
-
|
|
143
|
+
result.append(f"[{segment}]")
|
|
128
144
|
else:
|
|
129
145
|
# Member name: $['field']
|
|
130
146
|
# Escape single quotes in the string
|
|
131
|
-
escaped = str(
|
|
132
|
-
|
|
133
|
-
return "".join(
|
|
147
|
+
escaped = str(segment).replace("'", "\\'")
|
|
148
|
+
result.append(f"['{escaped}']")
|
|
149
|
+
return "".join(result)
|
|
134
150
|
|
|
135
151
|
# RFC 6901 JSON Pointer
|
|
136
|
-
return JsonPointer.from_parts(
|
|
152
|
+
return JsonPointer.from_parts(segments).path
|
|
137
153
|
|
|
138
154
|
def get_root(self) -> Any:
|
|
139
155
|
"""
|
|
@@ -142,4 +158,7 @@ class NodePath:
|
|
|
142
158
|
Returns:
|
|
143
159
|
Root datamodel object
|
|
144
160
|
"""
|
|
145
|
-
|
|
161
|
+
current = self
|
|
162
|
+
while current.parent_path is not None:
|
|
163
|
+
current = current.parent_path
|
|
164
|
+
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
|
|
File without changes
|