Skip to content

Commit 1e17754

Browse files
authored
Merge pull request #38 from network-tools/feat-xpath-search
added support for legacy and modern formats
2 parents ec22e5e + d45a356 commit 1e17754

8 files changed

Lines changed: 155 additions & 65 deletions

File tree

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.1.0] - 2025-12-28
9+
10+
### 🎉 Format System Refinement
11+
12+
This release refines the output format system with explicit naming for legacy and modern formats.
13+
14+
### Changed
15+
- 🔄 **Format parameter semantics** - Clarified format naming and behavior
16+
- `Parser()` now explicitly defaults to `'legacy'` format (backward compatible)
17+
- `Parser(output_format='legacy')` - OrderedDict with full command strings (backward compatible)
18+
- `Parser(output_format='json')` - dict with hierarchical structure (modern, XPath enabled)
19+
- `Parser(output_format='yaml')` - dict with hierarchical structure (modern, XPath enabled)
20+
- 🔄 **XPath support** - Now works with both 'json' and 'yaml' formats (any modern format)
21+
- 🔄 **Format validation** - Clear error messages for invalid format specifications
22+
23+
### Technical Details
24+
25+
**Breaking refinement** (minimal impact):
26+
- `output_format='json'` behavior changed from OrderedDict to dict with hierarchical structure
27+
- Since this feature was added hours ago (same day), no users are affected
28+
- Legacy behavior preserved via `output_format='legacy'` or `Parser()` (no params)
29+
30+
**Migration:**
31+
```python
32+
# v3.0 code (still works)
33+
p = Parser() # Returns OrderedDict with full keys
34+
35+
# v3.1 explicit legacy
36+
p = Parser(output_format='legacy') # Same as above
37+
38+
# v3.1 modern formats (hierarchical structure, XPath enabled)
39+
p = Parser(output_format='json') # dict with hierarchy
40+
p = Parser(output_format='yaml') # dict with hierarchy
41+
```
42+
43+
**Format comparison:**
44+
```python
45+
# Legacy format (OrderedDict with full keys)
46+
{'interface FastEthernet0/0': {'ip address 1.1.1.1': ''}}
47+
48+
# Modern formats (dict with hierarchy)
49+
{'interface': {'FastEthernet0/0': {'ip': {'address': '1.1.1.1'}}}}
50+
```
51+
852
## [3.0.0] - 2025-12-27
953

1054
### 🎉 Major Release - Modernization

README.md

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@ shconfparser is a vendor independent library where you can parse the following f
2828
- Table structure *`i.e. show ip interface`*
2929
- Data *`i.e. show version`*
3030

31-
YAML Format Output
31+
Modern Format (JSON/YAML) - Hierarchical Structure
3232

33-
![show run to YAML structure](https://raw.githubusercontent.com/kirankotari/shconfparser/master/asserts/img/sh_run_yaml.png)
33+
![show run to modern YAML format structure](https://raw.githubusercontent.com/kirankotari/shconfparser/master/asserts/img/sh_run_yaml.png)
34+
<br/>
35+
<br/>
36+
![show run to modern JSON format structure](https://raw.githubusercontent.com/kirankotari/shconfparser/master/asserts/img/sh_run_json.png)
3437

35-
Tree Structure
38+
Legacy Format - OrderedDict with Full Keys
3639

37-
![show run to tree structure](https://raw.githubusercontent.com/kirankotari/shconfparser/master/asserts/img/sh_run.png)
40+
![show run to legacy format structure](https://raw.githubusercontent.com/kirankotari/shconfparser/master/asserts/img/sh_run.png)
3841

3942
Table Structure
4043

@@ -67,12 +70,12 @@ uv pip install shconfparser
6770

6871
### Basic Usage
6972

70-
**Single show command with YAML format (recommended):**
73+
**Modern format (recommended - hierarchical structure with XPath):**
7174
```python
7275
from shconfparser.parser import Parser
7376

74-
# Use YAML format for cleaner output and XPath support
75-
p = Parser(output_format='yaml')
77+
# Use modern format for cleaner output and XPath support
78+
p = Parser(output_format='json') # or 'yaml'
7679
data = p.read('running_config.txt')
7780

7881
# Parse directly (no split needed for single show running command)
@@ -85,21 +88,24 @@ print(result.data) # 'R1'
8588
```
8689

8790
<details>
88-
<summary>Alternative: JSON format (backward compatible)</summary>
91+
<summary>Alternative: Legacy format (backward compatible)</summary>
8992

9093
```python
91-
p = Parser() # Default is JSON format (OrderedDict)
94+
p = Parser() # Defaults to 'legacy' format
95+
# or explicitly: Parser(output_format='legacy')
9296
data = p.read('running_config.txt')
9397
tree = p.parse_tree(data)
9498
print(p.dump(tree, indent=4))
99+
# Returns OrderedDict with full command strings as keys
100+
# Example: {'interface FastEthernet0/0': {...}}
95101
```
96102
</details>
97103

98104
**Multiple show commands in one file:**
99105
```python
100106
from shconfparser.parser import Parser
101107

102-
p = Parser(output_format='yaml') # YAML format recommended
108+
p = Parser(output_format='json') # Modern format recommended
103109
data = p.read('multiple_commands.txt') # Contains multiple show outputs
104110
data = p.split(data) # Split into separate commands
105111
data.keys()
@@ -216,48 +222,47 @@ print(match)
216222
# {'Device ID': 'R2', 'Local Intrfce': 'Fas 0/0', ...}
217223
```
218224

219-
### Output Format Selection (New in 3.0!)
225+
### Output Format Selection
220226

221-
Parse configurations to JSON (OrderedDict) or YAML-friendly dict structures:
227+
Parse configurations in legacy (OrderedDict) or modern (dict) hierarchical structures:
222228

223229
```python
224230
from shconfparser.parser import Parser
225231

226-
# Default: JSON format (OrderedDict - backward compatible)
227-
p = Parser()
232+
# Legacy format (backward compatible - OrderedDict with full keys)
233+
p = Parser() # Defaults to 'legacy'
234+
# or explicitly: Parser(output_format='legacy')
228235
data = p.read('running_config.txt')
229236
tree = p.parse_tree(data) # Returns OrderedDict
230237
print(type(tree)) # <class 'collections.OrderedDict'>
238+
# Example: {'interface FastEthernet0/0': {'ip address 1.1.1.1': ''}}
231239

232-
# YAML format: cleaner hierarchical structure
233-
p = Parser(output_format='yaml')
240+
# Modern formats: JSON or YAML (hierarchical dict structure)
241+
p = Parser(output_format='json') # Hierarchical dict
242+
# or: Parser(output_format='yaml') # Same structure, different name
234243
data = p.read('running_config.txt')
235-
tree_yaml = p.parse_tree(data) # Returns dict with nested structure
236-
print(type(tree_yaml)) # <class 'dict'>
244+
tree = p.parse_tree(data) # Returns dict
245+
print(type(tree)) # <class 'dict'>
246+
# Example: {'interface': {'FastEthernet0/0': {'ip': {'address': '1.1.1.1'}}}}
237247

238248
# Override format per call
239-
p = Parser() # Default is JSON
240-
tree_json = p.parse_tree(data) # OrderedDict
241-
tree_yaml = p.parse_tree(data, format='yaml') # dict
242-
243-
# YAML structure example:
244-
# Input: "interface FastEthernet0/0" with nested config
245-
# JSON: {"interface FastEthernet0/0": {...}}
246-
# YAML: {"interface": {"FastEthernet0/0": {...}}}
249+
p = Parser() # Legacy by default
250+
tree_legacy = p.parse_tree(data) # OrderedDict
251+
tree_json = p.parse_tree(data, format='json') # dict
247252
```
248253

249254
**Format Comparison:**
250255

251256
```python
252-
# JSON format (default) - preserves exact CLI structure
257+
# Legacy format - preserves exact CLI structure (OrderedDict)
253258
{
254259
"interface FastEthernet0/0": {
255260
"ip address 1.1.1.1 255.255.255.0": "",
256261
"duplex auto": ""
257262
}
258263
}
259264

260-
# YAML format - hierarchical and human-readable
265+
# Modern formats (json/yaml) - hierarchical and programmatic (dict)
261266
{
262267
"interface": {
263268
"FastEthernet0/0": {
@@ -270,12 +275,12 @@ tree_yaml = p.parse_tree(data, format='yaml') # dict
270275
}
271276
```
272277

273-
**Benefits of YAML format:**
278+
**Benefits of modern formats (json/yaml):**
274279
- Cleaner hierarchy for nested configurations
275280
- Better for programmatic access
276-
- Easier to convert to actual YAML files
281+
- XPath query support
282+
- Easier to convert to actual JSON/YAML files
277283
- Natural structure for complex configs
278-
- Required for XPath queries
279284

280285
### XPath Queries (New in 3.0!)
281286

asserts/img/sh_run_json.png

381 KB
Loading

docs/XPATH_GUIDE.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,20 @@ XPath queries provide a powerful way to search and extract data from network con
2828

2929
### Requirements
3030

31-
**XPath queries only work with YAML format:**
31+
**XPath queries work with modern formats (json or yaml):**
3232

3333
```python
34-
# ✅ Correct - YAML format required
35-
p = Parser(output_format='yaml')
34+
# ✅ Correct - JSON format (hierarchical dict)
35+
p = Parser(output_format='json')
3636
tree = p.parse_tree(data)
3737
result = p.xpath('/hostname')
3838

39-
# ❌ Wrong - JSON format not supported
40-
p = Parser(output_format='json')
39+
# ✅ Correct - YAML format (hierarchical dict, same structure as json)
40+
p = Parser(output_format='yaml')
41+
result = p.xpath('/hostname')
42+
43+
# ❌ Wrong - Legacy format not supported
44+
p = Parser() # Defaults to 'legacy'
4145
result = p.xpath('/hostname') # Returns error
4246
```
4347

@@ -46,8 +50,8 @@ result = p.xpath('/hostname') # Returns error
4650
```python
4751
from shconfparser import Parser
4852

49-
# Initialize parser with YAML format
50-
p = Parser(output_format='yaml')
53+
# Initialize parser with modern format (json or yaml)
54+
p = Parser(output_format='json') # or 'yaml' - both work
5155
data = p.read('running_config.txt')
5256
tree = p.parse_tree(data)
5357

@@ -435,7 +439,7 @@ p = Parser(output_format='json')
435439
result = p.xpath('/hostname') # Returns error
436440
```
437441

438-
**Solution:** Use `output_format='yaml'`
442+
**Solution:** Use `output_format='json'` or `output_format='yaml'` (modern formats)
439443

440444
### 2. No Attribute Selection
441445

@@ -488,10 +492,10 @@ result = p.xpath('hostname') # Missing leading /
488492
result = p.xpath('/hostname', context='invalid')
489493
# result.error: "Invalid context 'invalid'. Must be 'none', 'partial', or 'full'"
490494

491-
# JSON format error
492-
p = Parser(output_format='json')
495+
# Legacy format error
496+
p = Parser() # Defaults to 'legacy'
493497
result = p.xpath('/hostname')
494-
# result.error: "XPath queries only work with output_format='yaml'..."
498+
# result.error: "XPath requires modern format (json/yaml)..."
495499
```
496500

497501
## Performance Tips

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "shconfparser"
7-
version = "3.0.0"
7+
version = "3.1.0"
88
description = "Network configuration parser that translates show command outputs into structured data"
99
readme = "README.md"
1010
requires-python = ">=3.9"

shconfparser/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from .tree_parser import TreeParser
4242
from .xpath import XPath
4343

44-
__version__ = "3.0.0"
44+
__version__ = "3.1.0"
4545
__author__ = "Kiran Kumar Kotari"
4646
__email__ = "kirankotari@live.com"
4747

shconfparser/parser.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,32 @@ def __init__(
5050
self,
5151
log_level: int = logging.INFO,
5252
log_format: Optional[str] = None,
53-
output_format: str = "json",
53+
output_format: Optional[str] = None,
5454
) -> None:
5555
"""Initialize the Parser.
5656
5757
Args:
5858
log_level: Logging level (default: INFO)
5959
log_format: Custom log format string
60-
output_format: Default output format for parse_tree ('json' or 'yaml', default: 'json')
60+
output_format: Output structure format
61+
- None or 'legacy' (default): OrderedDict with full command strings
62+
Example: {'interface FastEthernet0/0': {'ip address 1.1.1.1': ''}}
63+
For backward compatibility. No XPath support.
64+
65+
- 'json': Hierarchical dict structure
66+
Example: {'interface': {'FastEthernet0/0': {'ip': {'address': '1.1.1.1'}}}}
67+
XPath support enabled. Clean programmatic access.
68+
69+
- 'yaml': Hierarchical dict structure (same as json)
70+
Example: {'interface': {'FastEthernet0/0': {'ip': {'address': '1.1.1.1'}}}}
71+
XPath support enabled. YAML-friendly output.
6172
"""
6273
# State for backward compatibility
6374
self.data: TreeData = OrderedDict()
6475
self.table: TableData = []
6576

66-
# Output format configuration
67-
self.output_format: str = output_format
77+
# Output format configuration (None defaults to 'legacy' for backward compatibility)
78+
self.output_format: str = output_format if output_format is not None else "legacy"
6879

6980
# Logging
7081
self.format: Optional[str] = log_format
@@ -104,30 +115,43 @@ def parse_tree(self, lines: List[str], format: Optional[str] = None) -> TreeData
104115
105116
Args:
106117
lines: Configuration lines with indentation
107-
format: Output format ('json' or 'yaml'). If None, uses self.output_format
118+
format: Output format ('legacy', 'json', or 'yaml'). If None, uses self.output_format
108119
109120
Returns:
110-
Nested OrderedDict (json format) or dict (yaml format) representing configuration hierarchy
121+
- 'legacy': OrderedDict with full command strings as keys
122+
- 'json' or 'yaml': dict with hierarchical structure
111123
112124
Example:
113-
>>> parser = Parser()
125+
>>> parser = Parser() # Defaults to 'legacy'
114126
>>> config = ['interface Ethernet0', ' ip address 1.1.1.1']
115-
>>> tree = parser.parse_tree(config) # Returns OrderedDict (JSON)
116-
>>> tree_yaml = parser.parse_tree(config, format='yaml') # Returns dict (YAML-friendly)
127+
>>> tree = parser.parse_tree(config) # Returns OrderedDict with full keys
128+
129+
>>> parser = Parser(output_format='json')
130+
>>> tree = parser.parse_tree(config) # Returns dict with hierarchy
117131
"""
118132
# Parse to OrderedDict first
119133
ordered_tree = self.tree_parser.parse_tree(lines)
120134

121135
# Transform based on format
122136
output_format = format if format is not None else self.output_format
123137

124-
if output_format == "yaml":
125-
yaml_tree = self._tree_to_yaml_structure(ordered_tree)
126-
self.data = yaml_tree # type: ignore[assignment] # Store YAML format for xpath
127-
return yaml_tree
128-
else:
129-
self.data = ordered_tree # Store JSON format
138+
# Validate format
139+
valid_formats = {"legacy", "json", "yaml"}
140+
if output_format not in valid_formats:
141+
raise ValueError(
142+
f"Invalid output_format '{output_format}'. "
143+
f"Must be one of: {', '.join(sorted(valid_formats))}"
144+
)
145+
146+
if output_format == "legacy":
147+
# Legacy format: OrderedDict with full command strings
148+
self.data = ordered_tree
130149
return ordered_tree
150+
else:
151+
# Modern formats (json/yaml): dict with hierarchical structure
152+
hierarchical_tree = self._tree_to_yaml_structure(ordered_tree)
153+
self.data = hierarchical_tree # type: ignore[assignment]
154+
return hierarchical_tree
131155

132156
def parse_tree_safe(self, lines: List[str]) -> TreeParseResult:
133157
"""Parse tree structure with structured result.
@@ -398,11 +422,14 @@ def xpath(
398422
query=query,
399423
)
400424

401-
# XPath only works with YAML format (dict, not OrderedDict)
402-
if self.output_format != "yaml":
425+
# XPath only works with modern formats (json/yaml)
426+
if self.output_format not in ("json", "yaml"):
403427
return XPathResult(
404428
success=False,
405-
error=f"XPath queries only work with output_format='yaml', current format is '{self.output_format}'",
429+
error=(
430+
f"XPath queries require modern format. Use output_format='json' or 'yaml'. "
431+
f"Current format is '{self.output_format}' (OrderedDict with full command strings)."
432+
),
406433
query=query,
407434
)
408435

0 commit comments

Comments
 (0)