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.
@@ -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
- parent: Any | None # Parent datamodel object
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
- ancestors: tuple[Any, ...] # Tuple of ancestor nodes (root first)
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
- parent=self.node,
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 not self.ancestors and self.parent_field is None:
122
+ if self.parent_path is None:
107
123
  return "$" if path_format == "jsonpath" else ""
108
124
 
109
- # Build parts list
110
- parts: list[str | int] = []
111
-
112
- # This is a simplified implementation that only captures one level
113
- # A full implementation would need to walk back through ancestors
114
- if self.parent_field:
115
- parts.append(self.parent_field)
116
-
117
- if self.parent_key is not None:
118
- # Both int (array index) and str (object key) work with from_parts
119
- parts.append(self.parent_key)
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
- segments = ["$"]
124
- for part in parts:
125
- if isinstance(part, int):
144
+ result = ["$"]
145
+ for segment in segments:
146
+ if isinstance(segment, int):
126
147
  # Array index: $[0]
127
- segments.append(f"[{part}]")
148
+ result.append(f"[{segment}]")
128
149
  else:
129
150
  # Member name: $['field']
130
151
  # Escape single quotes in the string
131
- escaped = str(part).replace("'", "\\'")
132
- segments.append(f"['{escaped}']")
133
- return "".join(segments)
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(parts).path
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
- return self.ancestors[0] if self.ancestors else self.node
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
- parent=None,
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=field_name,
168
+ parent_field=dict_field_name,
159
169
  parent_key=unwrapped_key,
160
170
  )
161
171
  result = _visit_node(visitor, child_path)
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jentic-openapi-traverse
3
- Version: 1.0.0a23
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.0a23
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
- # Path formatting
131
- print(f"JSONPointer: {path.format_path()}") # /paths/~1users/get
132
- print(f"JSONPath: {path.format_path(path_format='jsonpath')}") # $['paths']['/users']['get']
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:
@@ -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=_QzvTnxrSlyRc_QIajyEkaKEwyng7t1y_qx9dEXxwg8,3216
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=MUqqxzEjOQGzt-TPlhWyh40xXG5B3ABYKvMWxmKgU1Q,4715
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=aAObD_JFRnD2I6EqLgZo3KFL3jkoRxarMcnUqZeMpm0,8090
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.0a23.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
11
- jentic_openapi_traverse-1.0.0a23.dist-info/licenses/NOTICE,sha256=pAOGW-rGw9KNc2cuuLWZkfx0GSTV4TicbgBKZSLPMIs,168
12
- jentic_openapi_traverse-1.0.0a23.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
13
- jentic_openapi_traverse-1.0.0a23.dist-info/METADATA,sha256=mR38IoYfZFJ7_V-sswXIbQyxUbFypNK6V6q4lrge5EI,13572
14
- jentic_openapi_traverse-1.0.0a23.dist-info/RECORD,,
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,,