xmllens


Namexmllens JSON
Version 0.1.3 PyPI version JSON
download
home_pageNone
SummaryA lightweight library to compare XML documents with tolerance and ignore rules.
upload_time2025-10-23 20:32:28
maintainerNone
docs_urlNone
authorNone
requires_python>=3.9
licenseApache-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"
}
        
Elapsed time: 0.63542s