jentic-openapi-traverse 1.0.0a23__py3-none-any.whl → 1.0.0a25__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/introspection.py +24 -0
- jentic/apitools/openapi/traverse/datamodels/low/path.py +51 -27
- jentic/apitools/openapi/traverse/datamodels/low/traversal.py +13 -3
- {jentic_openapi_traverse-1.0.0a23.dist-info → jentic_openapi_traverse-1.0.0a25.dist-info}/METADATA +33 -6
- {jentic_openapi_traverse-1.0.0a23.dist-info → jentic_openapi_traverse-1.0.0a25.dist-info}/RECORD +8 -8
- {jentic_openapi_traverse-1.0.0a23.dist-info → jentic_openapi_traverse-1.0.0a25.dist-info}/WHEEL +0 -0
- {jentic_openapi_traverse-1.0.0a23.dist-info → jentic_openapi_traverse-1.0.0a25.dist-info}/licenses/LICENSE +0 -0
- {jentic_openapi_traverse-1.0.0a23.dist-info → jentic_openapi_traverse-1.0.0a25.dist-info}/licenses/NOTICE +0 -0
|
@@ -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)
|
{jentic_openapi_traverse-1.0.0a23.dist-info → jentic_openapi_traverse-1.0.0a25.dist-info}/METADATA
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jentic-openapi-traverse
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a25
|
|
4
4
|
Summary: Jentic OpenAPI Traversal Utilities
|
|
5
5
|
Author: Jentic
|
|
6
6
|
Author-email: Jentic <hello@jentic.com>
|
|
7
7
|
License-Expression: Apache-2.0
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
License-File: NOTICE
|
|
10
|
-
Requires-Dist: jentic-openapi-datamodels~=1.0.
|
|
10
|
+
Requires-Dist: jentic-openapi-datamodels~=1.0.0a25
|
|
11
11
|
Requires-Dist: jsonpointer~=3.0.0
|
|
12
12
|
Requires-Python: >=3.11
|
|
13
13
|
Project-URL: Homepage, https://github.com/jentic/jentic-openapi-tools
|
|
@@ -123,15 +123,42 @@ class PathInspector:
|
|
|
123
123
|
print(f"Parent field: {path.parent_field}") # e.g., "get", "post"
|
|
124
124
|
print(f"Parent key: {path.parent_key}") # e.g., "/users" (for path items)
|
|
125
125
|
|
|
126
|
-
# Ancestry
|
|
126
|
+
# Ancestry (computed properties)
|
|
127
|
+
print(f"Parent: {path.parent.__class__.__name__}")
|
|
127
128
|
print(f"Ancestors: {len(path.ancestors)}")
|
|
128
129
|
root = path.get_root()
|
|
129
130
|
|
|
130
|
-
#
|
|
131
|
-
print(f"JSONPointer: {path.format_path()}")
|
|
132
|
-
|
|
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']
|
|
133
152
|
```
|
|
134
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
|
+
|
|
135
162
|
### Enter/Leave Hooks
|
|
136
163
|
|
|
137
164
|
Use enter/leave hooks for pre/post processing logic:
|
{jentic_openapi_traverse-1.0.0a23.dist-info → jentic_openapi_traverse-1.0.0a25.dist-info}/RECORD
RENAMED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
jentic/apitools/openapi/traverse/datamodels/low/__init__.py,sha256=M0xRiYA2ErksACo8bKSVIu6PVx9aiYTDCHEkcTnmXAA,277
|
|
2
|
-
jentic/apitools/openapi/traverse/datamodels/low/introspection.py,sha256=
|
|
2
|
+
jentic/apitools/openapi/traverse/datamodels/low/introspection.py,sha256=iSACRXtpeWRwTx0gI5ZcuWtCGguUPn7eCun3JJQfziY,3970
|
|
3
3
|
jentic/apitools/openapi/traverse/datamodels/low/merge.py,sha256=Z7OpX14y740oC2ML_ylTE0TTh-ZbrEaliMU9s1bDFfM,4411
|
|
4
|
-
jentic/apitools/openapi/traverse/datamodels/low/path.py,sha256=
|
|
4
|
+
jentic/apitools/openapi/traverse/datamodels/low/path.py,sha256=g6nCpOaAq5koWd5KMhq-F285AhK-wmRX3WUxMZupfGs,5693
|
|
5
5
|
jentic/apitools/openapi/traverse/datamodels/low/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
jentic/apitools/openapi/traverse/datamodels/low/traversal.py,sha256=
|
|
6
|
+
jentic/apitools/openapi/traverse/datamodels/low/traversal.py,sha256=cAWnTQjmb5fKafbiEaJvv-FK-3fuip-EhdOLfZLVQjk,8774
|
|
7
7
|
jentic/apitools/openapi/traverse/json/__init__.py,sha256=1euUmpZviE_ECtpXYchpO8hZito2BINPjfSHMNqAU8k,326
|
|
8
8
|
jentic/apitools/openapi/traverse/json/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
jentic/apitools/openapi/traverse/json/traversal.py,sha256=1ouszn4S29X0iJaMxxb1neyClbWXqIKwFGhHROcpBSI,3524
|
|
10
|
-
jentic_openapi_traverse-1.0.
|
|
11
|
-
jentic_openapi_traverse-1.0.
|
|
12
|
-
jentic_openapi_traverse-1.0.
|
|
13
|
-
jentic_openapi_traverse-1.0.
|
|
14
|
-
jentic_openapi_traverse-1.0.
|
|
10
|
+
jentic_openapi_traverse-1.0.0a25.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
11
|
+
jentic_openapi_traverse-1.0.0a25.dist-info/licenses/NOTICE,sha256=pAOGW-rGw9KNc2cuuLWZkfx0GSTV4TicbgBKZSLPMIs,168
|
|
12
|
+
jentic_openapi_traverse-1.0.0a25.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
13
|
+
jentic_openapi_traverse-1.0.0a25.dist-info/METADATA,sha256=qf-IU6Vfl_fnQDfozRKPvzv1g3VnFQ7RZhRRLj3FTd4,14733
|
|
14
|
+
jentic_openapi_traverse-1.0.0a25.dist-info/RECORD,,
|
{jentic_openapi_traverse-1.0.0a23.dist-info → jentic_openapi_traverse-1.0.0a25.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|