| Name | xmllens JSON |
| Version |
0.1.3
JSON |
| download |
| home_page | None |
| Summary | A lightweight library to compare XML documents with tolerance and ignore rules. |
| upload_time | 2025-10-23 20:32:28 |
| maintainer | None |
| docs_url | None |
| author | None |
| requires_python | >=3.9 |
| license | Apache-2.0 |
| keywords |
xml
diff
compare
tolerance
ignore
xpath
|
| VCS |
|
| bugtrack_url |
|
| requirements |
No requirements were recorded.
|
| Travis-CI |
No Travis.
|
| coveralls test coverage |
No coveralls.
|
# xmllens
Deep structural comparison for XML documents with per-path numeric
tolerance and XPath-like targeting.
## Overview
`xmllens` is a lightweight Python library for comparing two XML
documents with **fine-grained tolerance control**.
It supports:
- ✅ Global absolute (`abs_tol`) and relative (`rel_tol`) numeric
tolerances
- ✅ Per-path tolerance overrides via XPath-like expressions
- ✅ Ignoring volatile or irrelevant XML elements
- ✅ Detailed debug logs that explain *why* two XMLs differ
It’s ideal for comparing configuration files, XML-based API payloads,
or serialized data models where small numeric drifts are expected.
## Installation
pip install xmllens
## Supported Path Patterns
xmllens implements a simplified subset of XPath syntax:
| Pattern | Description |
| ---------------------- | ------------------------------ |
| `/a/b/c` | Exact element path |
| `/items/item[1]/price` | Specific index |
| `/items/*/price` | Any element name |
| `//price` | Recursive descent |
| `/root/*` | Wildcard for any child element |
## Full API
```
compare_xml(
xml_a: str,
xml_b: str,
*,
ignore_fields: list[str] = None,
abs_tol: float = 0.0,
rel_tol: float = 0.0,
abs_tol_fields: dict[str, float] = None,
rel_tol_fields: dict[str, float] = None,
epsilon: float = 1e-12,
show_debug: bool = False,
) -> bool
```
| Parameter | Description |
| --------------- | --------------------------------------------- |
| `xml_a, xml_b` | XML documents as strings |
| `ignore_fields` | XPath-like patterns to skip during comparison |
| `abs_tol` | Global absolute numeric tolerance |
| `rel_tol` | Global relative numeric tolerance |
| `abs_tol_fields` | Per-path absolute tolerances |
| `rel_tol_fields` | Per-path relative tolerances |
| `epsilon` | Small float to absorb FP rounding errors |
| `show_debug` | Enable detailed comparison logs |
## Examples
```python
from xmllens import compare_xml
xml1 = "<sensor><temp>21.5</temp><humidity>48.0</humidity></sensor>"
xml2 = "<sensor><temp>21.7</temp><humidity>48.5</humidity></sensor>"
# Default tolerances
res = compare_xml(xml1, xml2, abs_tol=0.05, rel_tol=0.01, show_debug=True)
print(res) # False
```
```bash
### Output (debug)
[NUMERIC COMPARE] /sensor/temp: 21.5 vs 21.7 | diff=0.200000 | abs_tol=0.05 | rel_tol=0.01 | threshold=0.217000
[MATCH NUMERIC] /sensor/temp: within tolerance
[NUMERIC COMPARE] /sensor/humidity: 48.0 vs 48.5 | diff=0.500000 | abs_tol=0.05 | rel_tol=0.01 | threshold=0.485000
[FAIL NUMERIC] /sensor/humidity → diff=0.500000 > threshold=0.485000
[FAIL IN ELEMENT] /sensor/humidity
```
### Simple Value Mismatch
```python
xml1 = "<root><x>1</x></root>"
xml2 = "<root><x>2</x></root>"
result = compare_xml(xml1, xml2)
print(result) # False
```
### Tag Mismatch
```python
xml1 = "<root><x>1</x></root>"
xml2 = "<root><y>1</y></root>"
result = compare_xml(xml1, xml2)
print(result) # False
```
### Global Tolerances
#### Absolute Tolerance
```python
xml1 = "<sensor><temp>20.0</temp></sensor>"
xml2 = "<sensor><temp>20.05</temp></sensor>"
result = compare_xml(xml1, xml2, abs_tol=0.1)
print(result) # True
```
#### Relative Tolerance
```python
xml1 = "<sensor><humidity>100.0</humidity></sensor>"
xml2 = "<sensor><humidity>104.0</humidity></sensor>"
result = compare_xml(xml1, xml2, rel_tol=0.05)
print(result) # True (5% tolerance)
```
### Per-Path Tolerances
#### Per-Path Absolute Tolerance
```python
xml1 = "<root><a>1.0</a><b>2.0</b></root>"
xml2 = "<root><a>1.5</a><b>2.9</b></root>"
abs_tol_fields = {"/root/b": 1.0}
result = compare_xml(xml1, xml2, abs_tol=0.5, abs_tol_fields=abs_tol_fields)
print(result) # True
```
#### Per-Path Relative Tolerance
```python
xml1 = "<values><x>100</x><y>200</y></values>"
xml2 = "<values><x>110</x><y>210</y></values>"
rel_tol_fields = {"/values/x": 0.2} # 20%
result = compare_xml(xml1, xml2, rel_tol=0.05, rel_tol_fields=rel_tol_fields)
print(result) # True
```
### Ignoring fields
#### Simple Ignore Path
```python
xml1 = "<root><id>1</id><timestamp>now</timestamp></root>"
xml2 = "<root><id>1</id><timestamp>later</timestamp></root>"
ignore_fields = ["/root/timestamp"]
result = compare_xml(xml1, xml2, ignore_fields=ignore_fields)
print(result) # True
```
### More Examples
#### Ignore multiple fields with different patterns:
- Exact path: /user/profile/updated_at
- Wildcard: /devices/*/debug
- Recursive: //trace
```python
xml1 = """
<data>
<user>
<id>7</id>
<profile><updated_at>2025-10-14T10:00:00Z</updated_at><age>30</age></profile>
</user>
<devices>
<device><id>d1</id><debug>alpha</debug><temp>20.0</temp></device>
<device><id>d2</id><debug>beta</debug><temp>20.1</temp></device>
</devices>
<sessions>
<session><events><event><meta><trace>abc</trace></meta><value>10.0</value></event></events></session>
<session><events><event><meta><trace>def</trace></meta><value>10.5</value></event></events></session>
</sessions>
</data>
"""
xml2 = """
<data>
<user>
<id>7</id>
<profile><updated_at>2025-10-15T10:00:05Z</updated_at><age>30</age></profile>
</user>
<devices>
<device><id>d1</id><debug>changed</debug><temp>20.05</temp></device>
<device><id>d2</id><debug>changed</debug><temp>20.18</temp></device>
</devices>
<sessions>
<session><events><event><meta><trace>xyz</trace></meta><value>10.01</value></event></events></session>
<session><events><event><meta><trace>uvw</trace></meta><value>10.52</value></event></events></session>
</sessions>
</data>
"""
ignore_fields = [
"/data/user/profile/updated_at",
"/data/devices/*/debug",
"//trace",
]
result = compare_xml(
xml1, xml2,
ignore_fields=ignore_fields,
abs_tol=0.05,
rel_tol=0.02
)
print(result) # True
```
#### combining absolute and relative tolerances for different fields.
```python
xml1 = """
<station>
<id>ST-42</id>
<location>Paris</location>
<version>1.0</version>
<metrics>
<temperature>21.5</temperature>
<humidity>48.0</humidity>
<pressure>1013.2</pressure>
<wind_speed>5.4</wind_speed>
</metrics>
<status><battery_level>96.0</battery_level></status>
</station>
"""
xml2 = """
<station>
<id>ST-42</id>
<location>Paris</location>
<version>1.03</version>
<metrics>
<temperature>21.6</temperature>
<humidity>49.3</humidity>
<pressure>1013.5</pressure>
<wind_speed>5.6</wind_speed>
</metrics>
<status><battery_level>94.8</battery_level></status>
</station>
"""
abs_tol_fields = {
"/station/version": 0.1,
"/station/metrics/humidity": 2.0,
"/station/status/battery_level": 2.0,
}
rel_tol_fields = {
"/station/metrics/wind_speed": 0.05,
}
result = compare_xml(
xml1, xml2,
abs_tol=0.05,
rel_tol=0.01,
abs_tol_fields=abs_tol_fields,
rel_tol_fields=rel_tol_fields
)
print(result) # True
```
## Tips
- Elements are compared in order.
- Attributes are compared strictly.
- Whitespace is trimmed before comparison.
- To ignore volatile elements (timestamps, UUIDs, etc.), use ignore_fields.
## License
Apache License 2.0 — © 2025 Mohamed Tahri Contributions welcome 🤝
Raw data
{
"_id": null,
"home_page": null,
"name": "xmllens",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": null,
"keywords": "xml, diff, compare, tolerance, ignore, xpath",
"author": null,
"author_email": "Mohamed Tahri <simotahri1@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/b6/96/8e0fd5552152823bdbc73ff15c805cf4e1d07b37dcc841ab8086c2ae90f5/xmllens-0.1.3.tar.gz",
"platform": null,
"description": "# xmllens\n\nDeep structural comparison for XML documents with per-path numeric\ntolerance and XPath-like targeting.\n\n## Overview\n\n`xmllens` is a lightweight Python library for comparing two XML\ndocuments with **fine-grained tolerance control**.\n\nIt supports:\n\n- \u2705 Global absolute (`abs_tol`) and relative (`rel_tol`) numeric\n tolerances\n- \u2705 Per-path tolerance overrides via XPath-like expressions\n- \u2705 Ignoring volatile or irrelevant XML elements\n- \u2705 Detailed debug logs that explain *why* two XMLs differ\n\nIt\u2019s ideal for comparing configuration files, XML-based API payloads,\nor serialized data models where small numeric drifts are expected.\n\n## Installation\n\n pip install xmllens\n\n## Supported Path Patterns\n\nxmllens implements a simplified subset of XPath syntax:\n\n| Pattern | Description |\n| ---------------------- | ------------------------------ |\n| `/a/b/c` | Exact element path |\n| `/items/item[1]/price` | Specific index |\n| `/items/*/price` | Any element name |\n| `//price` | Recursive descent |\n| `/root/*` | Wildcard for any child element |\n\n## Full API\n\n```\ncompare_xml(\n xml_a: str,\n xml_b: str,\n *,\n ignore_fields: list[str] = None,\n abs_tol: float = 0.0,\n rel_tol: float = 0.0,\n abs_tol_fields: dict[str, float] = None,\n rel_tol_fields: dict[str, float] = None,\n epsilon: float = 1e-12,\n show_debug: bool = False,\n) -> bool\n```\n\n| Parameter | Description |\n| --------------- | --------------------------------------------- |\n| `xml_a, xml_b` | XML documents as strings |\n| `ignore_fields` | XPath-like patterns to skip during comparison |\n| `abs_tol` | Global absolute numeric tolerance |\n| `rel_tol` | Global relative numeric tolerance |\n| `abs_tol_fields` | Per-path absolute tolerances |\n| `rel_tol_fields` | Per-path relative tolerances |\n| `epsilon` | Small float to absorb FP rounding errors |\n| `show_debug` | Enable detailed comparison logs |\n\n## Examples\n\n```python\nfrom xmllens import compare_xml\n\nxml1 = \"<sensor><temp>21.5</temp><humidity>48.0</humidity></sensor>\"\nxml2 = \"<sensor><temp>21.7</temp><humidity>48.5</humidity></sensor>\"\n\n# Default tolerances\nres = compare_xml(xml1, xml2, abs_tol=0.05, rel_tol=0.01, show_debug=True)\nprint(res) # False\n```\n```bash\n### Output (debug)\n\n[NUMERIC COMPARE] /sensor/temp: 21.5 vs 21.7 | diff=0.200000 | abs_tol=0.05 | rel_tol=0.01 | threshold=0.217000\n[MATCH NUMERIC] /sensor/temp: within tolerance\n[NUMERIC COMPARE] /sensor/humidity: 48.0 vs 48.5 | diff=0.500000 | abs_tol=0.05 | rel_tol=0.01 | threshold=0.485000\n[FAIL NUMERIC] /sensor/humidity \u2192 diff=0.500000 > threshold=0.485000\n[FAIL IN ELEMENT] /sensor/humidity\n```\n\n### Simple Value Mismatch\n\n```python\nxml1 = \"<root><x>1</x></root>\"\nxml2 = \"<root><x>2</x></root>\"\n\nresult = compare_xml(xml1, xml2)\nprint(result) # False\n```\n\n### Tag Mismatch\n\n```python\nxml1 = \"<root><x>1</x></root>\"\nxml2 = \"<root><y>1</y></root>\"\n\nresult = compare_xml(xml1, xml2)\nprint(result) # False\n```\n\n### Global Tolerances\n#### Absolute Tolerance\n\n```python\nxml1 = \"<sensor><temp>20.0</temp></sensor>\"\nxml2 = \"<sensor><temp>20.05</temp></sensor>\"\n\nresult = compare_xml(xml1, xml2, abs_tol=0.1)\nprint(result) # True\n```\n\n#### Relative Tolerance\n\n```python\nxml1 = \"<sensor><humidity>100.0</humidity></sensor>\"\nxml2 = \"<sensor><humidity>104.0</humidity></sensor>\"\n\nresult = compare_xml(xml1, xml2, rel_tol=0.05)\nprint(result) # True (5% tolerance)\n```\n\n### Per-Path Tolerances\n#### Per-Path Absolute Tolerance\n\n```python\nxml1 = \"<root><a>1.0</a><b>2.0</b></root>\"\nxml2 = \"<root><a>1.5</a><b>2.9</b></root>\"\n\nabs_tol_fields = {\"/root/b\": 1.0}\n\nresult = compare_xml(xml1, xml2, abs_tol=0.5, abs_tol_fields=abs_tol_fields)\nprint(result) # True\n```\n\n#### Per-Path Relative Tolerance\n\n```python\nxml1 = \"<values><x>100</x><y>200</y></values>\"\nxml2 = \"<values><x>110</x><y>210</y></values>\"\n\nrel_tol_fields = {\"/values/x\": 0.2} # 20%\n\nresult = compare_xml(xml1, xml2, rel_tol=0.05, rel_tol_fields=rel_tol_fields)\nprint(result) # True\n```\n\n### Ignoring fields\n#### Simple Ignore Path\n\n```python\nxml1 = \"<root><id>1</id><timestamp>now</timestamp></root>\"\nxml2 = \"<root><id>1</id><timestamp>later</timestamp></root>\"\n\nignore_fields = [\"/root/timestamp\"]\n\nresult = compare_xml(xml1, xml2, ignore_fields=ignore_fields)\nprint(result) # True\n```\n\n### More Examples\n\n#### Ignore multiple fields with different patterns:\n\n- Exact path: /user/profile/updated_at\n\n- Wildcard: /devices/*/debug\n\n- Recursive: //trace\n\n```python\n\nxml1 = \"\"\"\n<data>\n <user>\n <id>7</id>\n <profile><updated_at>2025-10-14T10:00:00Z</updated_at><age>30</age></profile>\n </user>\n <devices>\n <device><id>d1</id><debug>alpha</debug><temp>20.0</temp></device>\n <device><id>d2</id><debug>beta</debug><temp>20.1</temp></device>\n </devices>\n <sessions>\n <session><events><event><meta><trace>abc</trace></meta><value>10.0</value></event></events></session>\n <session><events><event><meta><trace>def</trace></meta><value>10.5</value></event></events></session>\n </sessions>\n</data>\n\"\"\"\n\nxml2 = \"\"\"\n<data>\n <user>\n <id>7</id>\n <profile><updated_at>2025-10-15T10:00:05Z</updated_at><age>30</age></profile>\n </user>\n <devices>\n <device><id>d1</id><debug>changed</debug><temp>20.05</temp></device>\n <device><id>d2</id><debug>changed</debug><temp>20.18</temp></device>\n </devices>\n <sessions>\n <session><events><event><meta><trace>xyz</trace></meta><value>10.01</value></event></events></session>\n <session><events><event><meta><trace>uvw</trace></meta><value>10.52</value></event></events></session>\n </sessions>\n</data>\n\"\"\"\n\nignore_fields = [\n \"/data/user/profile/updated_at\",\n \"/data/devices/*/debug\",\n \"//trace\",\n]\n\nresult = compare_xml(\n xml1, xml2,\n ignore_fields=ignore_fields,\n abs_tol=0.05,\n rel_tol=0.02\n)\nprint(result) # True\n```\n\n#### combining absolute and relative tolerances for different fields.\n\n```python\n\nxml1 = \"\"\"\n<station>\n <id>ST-42</id>\n <location>Paris</location>\n <version>1.0</version>\n <metrics>\n <temperature>21.5</temperature>\n <humidity>48.0</humidity>\n <pressure>1013.2</pressure>\n <wind_speed>5.4</wind_speed>\n </metrics>\n <status><battery_level>96.0</battery_level></status>\n</station>\n\"\"\"\n\nxml2 = \"\"\"\n<station>\n <id>ST-42</id>\n <location>Paris</location>\n <version>1.03</version>\n <metrics>\n <temperature>21.6</temperature>\n <humidity>49.3</humidity>\n <pressure>1013.5</pressure>\n <wind_speed>5.6</wind_speed>\n </metrics>\n <status><battery_level>94.8</battery_level></status>\n</station>\n\"\"\"\n\nabs_tol_fields = {\n \"/station/version\": 0.1,\n \"/station/metrics/humidity\": 2.0,\n \"/station/status/battery_level\": 2.0,\n}\n\nrel_tol_fields = {\n \"/station/metrics/wind_speed\": 0.05,\n}\n\nresult = compare_xml(\n xml1, xml2,\n abs_tol=0.05,\n rel_tol=0.01,\n abs_tol_fields=abs_tol_fields,\n rel_tol_fields=rel_tol_fields\n)\nprint(result) # True\n```\n\n## Tips\n\n- Elements are compared in order.\n\n- Attributes are compared strictly.\n\n- Whitespace is trimmed before comparison.\n\n- To ignore volatile elements (timestamps, UUIDs, etc.), use ignore_fields.\n\n## License\n\nApache License 2.0 \u2014 \u00a9 2025 Mohamed Tahri Contributions welcome \ud83e\udd1d\n",
"bugtrack_url": null,
"license": "Apache-2.0",
"summary": "A lightweight library to compare XML documents with tolerance and ignore rules.",
"version": "0.1.3",
"project_urls": null,
"split_keywords": [
"xml",
" diff",
" compare",
" tolerance",
" ignore",
" xpath"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "37557b201198502936d63d31a17aa76de70feb8dd27e47d749dc44b1044ec92b",
"md5": "a8dffacb2aa5bb3fc2b4e921f460b69f",
"sha256": "82bd6048b81b7cc55e5d7c6add8bb2583c963ee774e64eadc7a11b72f7d171a6"
},
"downloads": -1,
"filename": "xmllens-0.1.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "a8dffacb2aa5bb3fc2b4e921f460b69f",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 10858,
"upload_time": "2025-10-23T20:32:27",
"upload_time_iso_8601": "2025-10-23T20:32:27.412233Z",
"url": "https://files.pythonhosted.org/packages/37/55/7b201198502936d63d31a17aa76de70feb8dd27e47d749dc44b1044ec92b/xmllens-0.1.3-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "b6968e0fd5552152823bdbc73ff15c805cf4e1d07b37dcc841ab8086c2ae90f5",
"md5": "6aa63ba05ec83c620fc3e963c24cbfae",
"sha256": "d605ebaef283584913b882c80621955da623ba934aaa105b2b0d306402b139e9"
},
"downloads": -1,
"filename": "xmllens-0.1.3.tar.gz",
"has_sig": false,
"md5_digest": "6aa63ba05ec83c620fc3e963c24cbfae",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 14603,
"upload_time": "2025-10-23T20:32:28",
"upload_time_iso_8601": "2025-10-23T20:32:28.564174Z",
"url": "https://files.pythonhosted.org/packages/b6/96/8e0fd5552152823bdbc73ff15c805cf4e1d07b37dcc841ab8086c2ae90f5/xmllens-0.1.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-10-23 20:32:28",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "xmllens"
}