jentic-openapi-traverse 1.0.0a23__py3-none-any.whl → 1.0.0a24__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.
@@ -19,13 +19,28 @@ class NodePath:
19
19
  """
20
20
 
21
21
  node: Any # Current datamodel object
22
- parent: Any | None # Parent datamodel object
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
- ancestors: tuple[Any, ...] # Tuple of ancestor nodes (root first)
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
- parent=self.node,
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 not self.ancestors and self.parent_field is None:
120
+ if self.parent_path is None:
107
121
  return "$" if path_format == "jsonpath" else ""
108
122
 
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)
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
- 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)
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
- segments = ["$"]
124
- for part in parts:
125
- if isinstance(part, int):
139
+ result = ["$"]
140
+ for segment in segments:
141
+ if isinstance(segment, int):
126
142
  # Array index: $[0]
127
- segments.append(f"[{part}]")
143
+ result.append(f"[{segment}]")
128
144
  else:
129
145
  # Member name: $['field']
130
146
  # Escape single quotes in the string
131
- escaped = str(part).replace("'", "\\'")
132
- segments.append(f"['{escaped}']")
133
- return "".join(segments)
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(parts).path
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
- return self.ancestors[0] if self.ancestors else self.node
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
- 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.0a24
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.0a24
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
2
  jentic/apitools/openapi/traverse/datamodels/low/introspection.py,sha256=_QzvTnxrSlyRc_QIajyEkaKEwyng7t1y_qx9dEXxwg8,3216
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=l-2w0uV4OobkswNfhBXkv9wgGMYyB4HEpPTivMmto8o,5442
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.0a24.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
11
+ jentic_openapi_traverse-1.0.0a24.dist-info/licenses/NOTICE,sha256=pAOGW-rGw9KNc2cuuLWZkfx0GSTV4TicbgBKZSLPMIs,168
12
+ jentic_openapi_traverse-1.0.0a24.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
13
+ jentic_openapi_traverse-1.0.0a24.dist-info/METADATA,sha256=m7DrnZ_zS1W1NHvPLFxAIx4_dLZxGAFrApk0mG5cEsg,14733
14
+ jentic_openapi_traverse-1.0.0a24.dist-info/RECORD,,