diffx-python 0.3.0__tar.gz
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.
- diffx_python-0.3.0/.gitignore +50 -0
- diffx_python-0.3.0/PKG-INFO +134 -0
- diffx_python-0.3.0/README.md +100 -0
- diffx_python-0.3.0/pyproject.toml +85 -0
- diffx_python-0.3.0/src/diffx/__init__.py +33 -0
- diffx_python-0.3.0/src/diffx/compat.py +56 -0
- diffx_python-0.3.0/src/diffx/diffx.py +244 -0
- diffx_python-0.3.0/src/diffx/installer.py +113 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Rust
|
|
2
|
+
/target/
|
|
3
|
+
**/*.rs.bk
|
|
4
|
+
|
|
5
|
+
# IDEs
|
|
6
|
+
.idea/
|
|
7
|
+
.vscode/
|
|
8
|
+
|
|
9
|
+
# OS
|
|
10
|
+
.DS_Store
|
|
11
|
+
thumbs.db
|
|
12
|
+
|
|
13
|
+
# Cargo
|
|
14
|
+
Cargo.lock
|
|
15
|
+
|
|
16
|
+
# Test intermediate files (generated during tests)
|
|
17
|
+
/tests/output/
|
|
18
|
+
**/diff_report_v*.json
|
|
19
|
+
|
|
20
|
+
# Node.js
|
|
21
|
+
node_modules/
|
|
22
|
+
npm-debug.log*
|
|
23
|
+
yarn-debug.log*
|
|
24
|
+
yarn-error.log*
|
|
25
|
+
lerna-debug.log*
|
|
26
|
+
.pnpm-debug.log*
|
|
27
|
+
**/dist/
|
|
28
|
+
**/build/
|
|
29
|
+
|
|
30
|
+
# Python
|
|
31
|
+
__pycache__/
|
|
32
|
+
*.py[cod]
|
|
33
|
+
*~
|
|
34
|
+
*.so
|
|
35
|
+
.Python
|
|
36
|
+
.env
|
|
37
|
+
.venv
|
|
38
|
+
env/
|
|
39
|
+
venv/
|
|
40
|
+
ENV/
|
|
41
|
+
env.bak/
|
|
42
|
+
venv.bak/
|
|
43
|
+
*.egg-info/
|
|
44
|
+
.pytest_cache/
|
|
45
|
+
.coverage
|
|
46
|
+
htmlcov/
|
|
47
|
+
|
|
48
|
+
# Binary downloads
|
|
49
|
+
**/packages/*/bin/
|
|
50
|
+
**/packages/*/temp/
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: diffx-python
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python wrapper for diffx - semantic diff for structured data
|
|
5
|
+
Project-URL: Homepage, https://github.com/kako-jun/diffx
|
|
6
|
+
Project-URL: Repository, https://github.com/kako-jun/diffx
|
|
7
|
+
Project-URL: Issues, https://github.com/kako-jun/diffx/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/kako-jun/diffx/tree/main/docs
|
|
9
|
+
Author-email: kako-jun <kako.jun.42@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: comparison,csv,diff,ini,json,semantic,structured-data,toml,xml,yaml
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Text Processing
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: black; extra == 'dev'
|
|
28
|
+
Requires-Dist: isort; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=6.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# diffx
|
|
36
|
+
|
|
37
|
+
Python wrapper for the `diffx` CLI tool - semantic diff for structured data.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install diffx-py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This will automatically download the appropriate `diffx` binary for your system from GitHub Releases.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Modern API (Recommended)
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import diffx
|
|
53
|
+
|
|
54
|
+
# Compare two JSON files
|
|
55
|
+
result = diffx.diff('file1.json', 'file2.json')
|
|
56
|
+
print(result)
|
|
57
|
+
|
|
58
|
+
# Get structured output as JSON
|
|
59
|
+
json_result = diffx.diff(
|
|
60
|
+
'config1.yaml',
|
|
61
|
+
'config2.yaml',
|
|
62
|
+
diffx.DiffOptions(format='yaml', output='json')
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
for diff_item in json_result:
|
|
66
|
+
if diff_item.added:
|
|
67
|
+
print(f"Added: {diff_item.added}")
|
|
68
|
+
elif diff_item.modified:
|
|
69
|
+
print(f"Modified: {diff_item.modified}")
|
|
70
|
+
|
|
71
|
+
# Compare directory trees
|
|
72
|
+
dir_result = diffx.diff(
|
|
73
|
+
'dir1/',
|
|
74
|
+
'dir2/',
|
|
75
|
+
diffx.DiffOptions(recursive=True, path='config')
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Compare strings directly
|
|
79
|
+
json1 = '{"name": "Alice", "age": 30}'
|
|
80
|
+
json2 = '{"name": "Alice", "age": 31}'
|
|
81
|
+
string_result = diffx.diff_string(
|
|
82
|
+
json1, json2, 'json',
|
|
83
|
+
diffx.DiffOptions(output='json')
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Legacy API (Backward Compatibility)
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from diffx import run_diffx
|
|
91
|
+
|
|
92
|
+
# Compare two JSON files (legacy)
|
|
93
|
+
result = run_diffx(["file1.json", "file2.json"])
|
|
94
|
+
|
|
95
|
+
if result.returncode == 0:
|
|
96
|
+
print("No differences found.")
|
|
97
|
+
else:
|
|
98
|
+
print("Differences found:")
|
|
99
|
+
print(result.stdout)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Features
|
|
103
|
+
|
|
104
|
+
- **Multiple formats**: JSON, YAML, TOML, XML, INI, CSV
|
|
105
|
+
- **Smart diffing**: Understands structure, not just text
|
|
106
|
+
- **Flexible output**: CLI, JSON, YAML, unified diff formats
|
|
107
|
+
- **Advanced options**:
|
|
108
|
+
- Regex-based key filtering
|
|
109
|
+
- Floating-point tolerance
|
|
110
|
+
- Array element identification
|
|
111
|
+
- Path-based filtering
|
|
112
|
+
- **Cross-platform**: Automatically downloads the right binary
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
To install in development mode with uv:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uv venv
|
|
120
|
+
source .venv/bin/activate
|
|
121
|
+
uv pip install -e .[dev]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Manual Binary Installation
|
|
125
|
+
|
|
126
|
+
If automatic download fails:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
diffx-download-binary
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# diffx
|
|
2
|
+
|
|
3
|
+
Python wrapper for the `diffx` CLI tool - semantic diff for structured data.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install diffx-py
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This will automatically download the appropriate `diffx` binary for your system from GitHub Releases.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Modern API (Recommended)
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import diffx
|
|
19
|
+
|
|
20
|
+
# Compare two JSON files
|
|
21
|
+
result = diffx.diff('file1.json', 'file2.json')
|
|
22
|
+
print(result)
|
|
23
|
+
|
|
24
|
+
# Get structured output as JSON
|
|
25
|
+
json_result = diffx.diff(
|
|
26
|
+
'config1.yaml',
|
|
27
|
+
'config2.yaml',
|
|
28
|
+
diffx.DiffOptions(format='yaml', output='json')
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
for diff_item in json_result:
|
|
32
|
+
if diff_item.added:
|
|
33
|
+
print(f"Added: {diff_item.added}")
|
|
34
|
+
elif diff_item.modified:
|
|
35
|
+
print(f"Modified: {diff_item.modified}")
|
|
36
|
+
|
|
37
|
+
# Compare directory trees
|
|
38
|
+
dir_result = diffx.diff(
|
|
39
|
+
'dir1/',
|
|
40
|
+
'dir2/',
|
|
41
|
+
diffx.DiffOptions(recursive=True, path='config')
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Compare strings directly
|
|
45
|
+
json1 = '{"name": "Alice", "age": 30}'
|
|
46
|
+
json2 = '{"name": "Alice", "age": 31}'
|
|
47
|
+
string_result = diffx.diff_string(
|
|
48
|
+
json1, json2, 'json',
|
|
49
|
+
diffx.DiffOptions(output='json')
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Legacy API (Backward Compatibility)
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from diffx import run_diffx
|
|
57
|
+
|
|
58
|
+
# Compare two JSON files (legacy)
|
|
59
|
+
result = run_diffx(["file1.json", "file2.json"])
|
|
60
|
+
|
|
61
|
+
if result.returncode == 0:
|
|
62
|
+
print("No differences found.")
|
|
63
|
+
else:
|
|
64
|
+
print("Differences found:")
|
|
65
|
+
print(result.stdout)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Features
|
|
69
|
+
|
|
70
|
+
- **Multiple formats**: JSON, YAML, TOML, XML, INI, CSV
|
|
71
|
+
- **Smart diffing**: Understands structure, not just text
|
|
72
|
+
- **Flexible output**: CLI, JSON, YAML, unified diff formats
|
|
73
|
+
- **Advanced options**:
|
|
74
|
+
- Regex-based key filtering
|
|
75
|
+
- Floating-point tolerance
|
|
76
|
+
- Array element identification
|
|
77
|
+
- Path-based filtering
|
|
78
|
+
- **Cross-platform**: Automatically downloads the right binary
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
To install in development mode with uv:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uv venv
|
|
86
|
+
source .venv/bin/activate
|
|
87
|
+
uv pip install -e .[dev]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Manual Binary Installation
|
|
91
|
+
|
|
92
|
+
If automatic download fails:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
diffx-download-binary
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "diffx-python"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Python wrapper for diffx - semantic diff for structured data"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "kako-jun", email = "kako.jun.42@gmail.com" }
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Topic :: Text Processing",
|
|
27
|
+
"Topic :: Utilities"
|
|
28
|
+
]
|
|
29
|
+
keywords = [
|
|
30
|
+
"diff",
|
|
31
|
+
"semantic",
|
|
32
|
+
"json",
|
|
33
|
+
"yaml",
|
|
34
|
+
"toml",
|
|
35
|
+
"xml",
|
|
36
|
+
"ini",
|
|
37
|
+
"csv",
|
|
38
|
+
"structured-data",
|
|
39
|
+
"comparison"
|
|
40
|
+
]
|
|
41
|
+
requires-python = ">=3.8"
|
|
42
|
+
dependencies = []
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/kako-jun/diffx"
|
|
46
|
+
Repository = "https://github.com/kako-jun/diffx"
|
|
47
|
+
Issues = "https://github.com/kako-jun/diffx/issues"
|
|
48
|
+
Documentation = "https://github.com/kako-jun/diffx/tree/main/docs"
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest >= 6.0",
|
|
53
|
+
"pytest-cov",
|
|
54
|
+
"black",
|
|
55
|
+
"isort",
|
|
56
|
+
"mypy",
|
|
57
|
+
"ruff"
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[project.scripts]
|
|
61
|
+
diffx-download-binary = "diffx.installer:main"
|
|
62
|
+
|
|
63
|
+
[tool.hatch.build.targets.wheel]
|
|
64
|
+
packages = ["src/diffx"]
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.sdist]
|
|
67
|
+
include = [
|
|
68
|
+
"/src",
|
|
69
|
+
"/scripts",
|
|
70
|
+
"/README.md"
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 88
|
|
75
|
+
target-version = "py38"
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = ["E", "F", "W", "I", "N", "UP", "YTT", "ANN", "S", "BLE", "FBT", "B", "A", "COM", "C4", "DTZ", "T10", "ISC", "ICN", "G", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET", "SLF", "SIM", "TID", "TCH", "ARG", "PTH", "ERA", "PGH", "PL", "TRY", "NPY", "RUF"]
|
|
79
|
+
ignore = ["ANN101", "ANN102", "COM812", "ISC001"]
|
|
80
|
+
|
|
81
|
+
[tool.mypy]
|
|
82
|
+
python_version = "3.8"
|
|
83
|
+
warn_return_any = true
|
|
84
|
+
warn_unused_configs = true
|
|
85
|
+
disallow_untyped_defs = true
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
diffx: Python wrapper for the diffx CLI tool
|
|
3
|
+
|
|
4
|
+
This package provides a Python interface to the diffx CLI tool for semantic
|
|
5
|
+
diffing of structured data formats like JSON, YAML, TOML, XML, INI, and CSV.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .diffx import (
|
|
9
|
+
diff,
|
|
10
|
+
diff_string,
|
|
11
|
+
is_diffx_available,
|
|
12
|
+
DiffOptions,
|
|
13
|
+
DiffResult,
|
|
14
|
+
DiffError,
|
|
15
|
+
Format,
|
|
16
|
+
OutputFormat,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# For backward compatibility with existing diffx_python users
|
|
20
|
+
from .compat import run_diffx
|
|
21
|
+
|
|
22
|
+
__version__ = "0.3.0"
|
|
23
|
+
__all__ = [
|
|
24
|
+
"diff",
|
|
25
|
+
"diff_string",
|
|
26
|
+
"is_diffx_available",
|
|
27
|
+
"DiffOptions",
|
|
28
|
+
"DiffResult",
|
|
29
|
+
"DiffError",
|
|
30
|
+
"Format",
|
|
31
|
+
"OutputFormat",
|
|
32
|
+
"run_diffx", # Backward compatibility
|
|
33
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backward compatibility layer for existing diffx_python users
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import platform
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_diffx(args):
|
|
13
|
+
"""
|
|
14
|
+
Run diffx command with given arguments (backward compatibility)
|
|
15
|
+
|
|
16
|
+
This function maintains compatibility with the original diffx_python API.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
args: List of command line arguments for diffx
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
subprocess.CompletedProcess object with stdout, stderr, and returncode
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
>>> result = run_diffx(["file1.json", "file2.json"])
|
|
26
|
+
>>> print(result.stdout)
|
|
27
|
+
"""
|
|
28
|
+
# Determine the path to the diffx binary
|
|
29
|
+
package_dir = Path(__file__).parent.parent.parent
|
|
30
|
+
binary_name = "diffx.exe" if platform.system() == "Windows" else "diffx"
|
|
31
|
+
diffx_binary_path = package_dir / "bin" / binary_name
|
|
32
|
+
|
|
33
|
+
# Fall back to system PATH if local binary doesn't exist
|
|
34
|
+
if not diffx_binary_path.exists():
|
|
35
|
+
diffx_binary_path = "diffx"
|
|
36
|
+
|
|
37
|
+
command = [str(diffx_binary_path)] + args
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
|
41
|
+
|
|
42
|
+
if result.returncode != 0 and result.stderr:
|
|
43
|
+
print(f"Error running diffx: {result.stderr}", file=sys.stderr)
|
|
44
|
+
|
|
45
|
+
return result
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
# Create a mock result object for consistency
|
|
48
|
+
class MockResult:
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self.stdout = ""
|
|
51
|
+
self.stderr = "diffx binary not found. Please ensure the package is installed correctly."
|
|
52
|
+
self.returncode = -1
|
|
53
|
+
|
|
54
|
+
result = MockResult()
|
|
55
|
+
print(f"Error: {result.stderr}", file=sys.stderr)
|
|
56
|
+
return result
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main diffx wrapper implementation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Union, List, Dict, Any, Optional, Literal
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Type definitions
|
|
16
|
+
Format = Literal["json", "yaml", "toml", "xml", "ini", "csv"]
|
|
17
|
+
OutputFormat = Literal["cli", "json", "yaml", "unified"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class DiffOptions:
|
|
22
|
+
"""Options for the diff operation"""
|
|
23
|
+
format: Optional[Format] = None
|
|
24
|
+
output: Optional[OutputFormat] = None
|
|
25
|
+
recursive: bool = False
|
|
26
|
+
path: Optional[str] = None
|
|
27
|
+
ignore_keys_regex: Optional[str] = None
|
|
28
|
+
epsilon: Optional[float] = None
|
|
29
|
+
array_id_key: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DiffResult:
|
|
33
|
+
"""Result of a diff operation when output format is 'json'"""
|
|
34
|
+
def __init__(self, data: Dict[str, Any]):
|
|
35
|
+
self.data = data
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def added(self) -> Optional[tuple]:
|
|
39
|
+
"""Get Added result if present"""
|
|
40
|
+
return tuple(self.data["Added"]) if "Added" in self.data else None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def removed(self) -> Optional[tuple]:
|
|
44
|
+
"""Get Removed result if present"""
|
|
45
|
+
return tuple(self.data["Removed"]) if "Removed" in self.data else None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def modified(self) -> Optional[tuple]:
|
|
49
|
+
"""Get Modified result if present"""
|
|
50
|
+
return tuple(self.data["Modified"]) if "Modified" in self.data else None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def type_changed(self) -> Optional[tuple]:
|
|
54
|
+
"""Get TypeChanged result if present"""
|
|
55
|
+
return tuple(self.data["TypeChanged"]) if "TypeChanged" in self.data else None
|
|
56
|
+
|
|
57
|
+
def __repr__(self) -> str:
|
|
58
|
+
return f"DiffResult({self.data})"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DiffError(Exception):
|
|
62
|
+
"""Error thrown when diffx command fails"""
|
|
63
|
+
def __init__(self, message: str, exit_code: int, stderr: str):
|
|
64
|
+
super().__init__(message)
|
|
65
|
+
self.exit_code = exit_code
|
|
66
|
+
self.stderr = stderr
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_diffx_binary_path() -> str:
|
|
70
|
+
"""Get the path to the diffx binary"""
|
|
71
|
+
# Check if local binary exists (installed via postinstall)
|
|
72
|
+
package_dir = Path(__file__).parent.parent.parent
|
|
73
|
+
binary_name = "diffx.exe" if platform.system() == "Windows" else "diffx"
|
|
74
|
+
local_binary_path = package_dir / "bin" / binary_name
|
|
75
|
+
|
|
76
|
+
if local_binary_path.exists():
|
|
77
|
+
return str(local_binary_path)
|
|
78
|
+
|
|
79
|
+
# Fall back to system PATH
|
|
80
|
+
return "diffx"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _execute_diffx(args: List[str]) -> tuple[str, str]:
|
|
84
|
+
"""Execute diffx command and return stdout, stderr"""
|
|
85
|
+
diffx_path = _get_diffx_binary_path()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
[diffx_path] + args,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
check=True
|
|
93
|
+
)
|
|
94
|
+
return result.stdout, result.stderr
|
|
95
|
+
except subprocess.CalledProcessError as e:
|
|
96
|
+
raise DiffError(
|
|
97
|
+
f"diffx exited with code {e.returncode}",
|
|
98
|
+
e.returncode,
|
|
99
|
+
e.stderr or ""
|
|
100
|
+
)
|
|
101
|
+
except FileNotFoundError:
|
|
102
|
+
raise DiffError(
|
|
103
|
+
"diffx command not found. Please install diffx CLI tool.",
|
|
104
|
+
-1,
|
|
105
|
+
""
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def diff(
|
|
110
|
+
input1: str,
|
|
111
|
+
input2: str,
|
|
112
|
+
options: Optional[DiffOptions] = None
|
|
113
|
+
) -> Union[str, List[DiffResult]]:
|
|
114
|
+
"""
|
|
115
|
+
Compare two files or directories using diffx
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
input1: Path to first file/directory or '-' for stdin
|
|
119
|
+
input2: Path to second file/directory
|
|
120
|
+
options: Comparison options
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
String output for CLI format, or list of DiffResult for JSON format
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
>>> result = diff('file1.json', 'file2.json')
|
|
127
|
+
>>> print(result)
|
|
128
|
+
|
|
129
|
+
>>> json_result = diff('config1.yaml', 'config2.yaml',
|
|
130
|
+
... DiffOptions(format='yaml', output='json'))
|
|
131
|
+
>>> for diff_item in json_result:
|
|
132
|
+
... print(diff_item)
|
|
133
|
+
|
|
134
|
+
>>> dir_result = diff('dir1/', 'dir2/',
|
|
135
|
+
... DiffOptions(recursive=True, path='config'))
|
|
136
|
+
"""
|
|
137
|
+
if options is None:
|
|
138
|
+
options = DiffOptions()
|
|
139
|
+
|
|
140
|
+
args = [input1, input2]
|
|
141
|
+
|
|
142
|
+
# Add format option
|
|
143
|
+
if options.format:
|
|
144
|
+
args.extend(["--format", options.format])
|
|
145
|
+
|
|
146
|
+
# Add output format option
|
|
147
|
+
if options.output:
|
|
148
|
+
args.extend(["--output", options.output])
|
|
149
|
+
|
|
150
|
+
# Add recursive option
|
|
151
|
+
if options.recursive:
|
|
152
|
+
args.append("--recursive")
|
|
153
|
+
|
|
154
|
+
# Add path filter option
|
|
155
|
+
if options.path:
|
|
156
|
+
args.extend(["--path", options.path])
|
|
157
|
+
|
|
158
|
+
# Add ignore keys regex option
|
|
159
|
+
if options.ignore_keys_regex:
|
|
160
|
+
args.extend(["--ignore-keys-regex", options.ignore_keys_regex])
|
|
161
|
+
|
|
162
|
+
# Add epsilon option
|
|
163
|
+
if options.epsilon is not None:
|
|
164
|
+
args.extend(["--epsilon", str(options.epsilon)])
|
|
165
|
+
|
|
166
|
+
# Add array ID key option
|
|
167
|
+
if options.array_id_key:
|
|
168
|
+
args.extend(["--array-id-key", options.array_id_key])
|
|
169
|
+
|
|
170
|
+
stdout, stderr = _execute_diffx(args)
|
|
171
|
+
|
|
172
|
+
# If output format is JSON, parse the result
|
|
173
|
+
if options.output == "json":
|
|
174
|
+
try:
|
|
175
|
+
json_data = json.loads(stdout)
|
|
176
|
+
return [DiffResult(item) for item in json_data]
|
|
177
|
+
except json.JSONDecodeError as e:
|
|
178
|
+
raise DiffError(f"Failed to parse JSON output: {e}", -1, "")
|
|
179
|
+
|
|
180
|
+
# Return raw output for other formats
|
|
181
|
+
return stdout
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def diff_string(
|
|
185
|
+
content1: str,
|
|
186
|
+
content2: str,
|
|
187
|
+
format: Format,
|
|
188
|
+
options: Optional[DiffOptions] = None
|
|
189
|
+
) -> Union[str, List[DiffResult]]:
|
|
190
|
+
"""
|
|
191
|
+
Compare two strings directly (writes to temporary files)
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
content1: First content string
|
|
195
|
+
content2: Second content string
|
|
196
|
+
format: Content format
|
|
197
|
+
options: Comparison options
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
String output for CLI format, or list of DiffResult for JSON format
|
|
201
|
+
|
|
202
|
+
Examples:
|
|
203
|
+
>>> json1 = '{"name": "Alice", "age": 30}'
|
|
204
|
+
>>> json2 = '{"name": "Alice", "age": 31}'
|
|
205
|
+
>>> result = diff_string(json1, json2, 'json',
|
|
206
|
+
... DiffOptions(output='json'))
|
|
207
|
+
>>> print(result)
|
|
208
|
+
"""
|
|
209
|
+
if options is None:
|
|
210
|
+
options = DiffOptions()
|
|
211
|
+
|
|
212
|
+
# Ensure format is set
|
|
213
|
+
options.format = format
|
|
214
|
+
|
|
215
|
+
# Create temporary files
|
|
216
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
217
|
+
tmp_file1 = Path(tmp_dir) / f"file1.{format}"
|
|
218
|
+
tmp_file2 = Path(tmp_dir) / f"file2.{format}"
|
|
219
|
+
|
|
220
|
+
# Write content to temporary files
|
|
221
|
+
tmp_file1.write_text(content1, encoding="utf-8")
|
|
222
|
+
tmp_file2.write_text(content2, encoding="utf-8")
|
|
223
|
+
|
|
224
|
+
# Perform diff
|
|
225
|
+
return diff(str(tmp_file1), str(tmp_file2), options)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def is_diffx_available() -> bool:
|
|
229
|
+
"""
|
|
230
|
+
Check if diffx command is available in the system
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True if diffx is available, False otherwise
|
|
234
|
+
|
|
235
|
+
Examples:
|
|
236
|
+
>>> if not is_diffx_available():
|
|
237
|
+
... print("Please install diffx CLI tool")
|
|
238
|
+
... exit(1)
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
_execute_diffx(["--version"])
|
|
242
|
+
return True
|
|
243
|
+
except DiffError:
|
|
244
|
+
return False
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Binary installer for diffx."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
import urllib.request
|
|
12
|
+
import zipfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
DIFFX_VERSION = "0.3.0"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_platform_info():
|
|
19
|
+
"""Get platform-specific download information."""
|
|
20
|
+
system = platform.system().lower()
|
|
21
|
+
machine = platform.machine().lower()
|
|
22
|
+
|
|
23
|
+
if system == "windows":
|
|
24
|
+
return "diffx-windows-x86_64.zip", "diffx.exe"
|
|
25
|
+
elif system == "darwin":
|
|
26
|
+
if machine in ["arm64", "aarch64"]:
|
|
27
|
+
return "diffx-macos-aarch64.tar.gz", "diffx"
|
|
28
|
+
else:
|
|
29
|
+
return "diffx-macos-x86_64.tar.gz", "diffx"
|
|
30
|
+
elif system == "linux":
|
|
31
|
+
return "diffx-linux-x86_64.tar.gz", "diffx"
|
|
32
|
+
else:
|
|
33
|
+
raise RuntimeError(f"Unsupported platform: {system}-{machine}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def download_file(url: str, dest_path: Path) -> None:
|
|
37
|
+
"""Download a file from URL to destination."""
|
|
38
|
+
print(f"Downloading diffx binary from {url}...")
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
with urllib.request.urlopen(url) as response:
|
|
42
|
+
with open(dest_path, 'wb') as dest_file:
|
|
43
|
+
shutil.copyfileobj(response, dest_file)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
raise RuntimeError(f"Failed to download {url}: {e}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def extract_archive(archive_path: Path, extract_dir: Path) -> None:
|
|
49
|
+
"""Extract archive file."""
|
|
50
|
+
print("Extracting binary...")
|
|
51
|
+
|
|
52
|
+
if archive_path.suffix == '.zip':
|
|
53
|
+
with zipfile.ZipFile(archive_path, 'r') as zip_file:
|
|
54
|
+
zip_file.extractall(extract_dir)
|
|
55
|
+
elif archive_path.name.endswith('.tar.gz'):
|
|
56
|
+
with tarfile.open(archive_path, 'r:gz') as tar_file:
|
|
57
|
+
tar_file.extractall(extract_dir)
|
|
58
|
+
else:
|
|
59
|
+
raise RuntimeError(f"Unsupported archive format: {archive_path}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main():
|
|
63
|
+
"""Main function to download and install diffx binary."""
|
|
64
|
+
try:
|
|
65
|
+
# Get the package directory (where this script will be installed)
|
|
66
|
+
if hasattr(sys, '_MEIPASS'):
|
|
67
|
+
# If running from PyInstaller bundle
|
|
68
|
+
package_dir = Path(sys._MEIPASS).parent
|
|
69
|
+
else:
|
|
70
|
+
# Normal installation
|
|
71
|
+
package_dir = Path(__file__).parent.parent.parent
|
|
72
|
+
|
|
73
|
+
bin_dir = package_dir / "bin"
|
|
74
|
+
|
|
75
|
+
# Get platform info
|
|
76
|
+
archive_name, binary_name = get_platform_info()
|
|
77
|
+
binary_path = bin_dir / binary_name
|
|
78
|
+
|
|
79
|
+
# Skip download if binary already exists
|
|
80
|
+
if binary_path.exists():
|
|
81
|
+
print("diffx binary already exists, skipping download.")
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
# Create bin directory
|
|
85
|
+
bin_dir.mkdir(exist_ok=True)
|
|
86
|
+
|
|
87
|
+
# Download URL
|
|
88
|
+
download_url = f"https://github.com/kako-jun/diffx/releases/download/v{DIFFX_VERSION}/{archive_name}"
|
|
89
|
+
|
|
90
|
+
# Download to temporary file
|
|
91
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
92
|
+
temp_path = Path(temp_dir)
|
|
93
|
+
archive_path = temp_path / archive_name
|
|
94
|
+
|
|
95
|
+
download_file(download_url, archive_path)
|
|
96
|
+
extract_archive(archive_path, bin_dir)
|
|
97
|
+
|
|
98
|
+
# Make binary executable on Unix systems
|
|
99
|
+
if platform.system() != "Windows":
|
|
100
|
+
binary_path.chmod(0o755)
|
|
101
|
+
|
|
102
|
+
print(f"✅ diffx binary installed successfully at {binary_path}")
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
except Exception as error:
|
|
106
|
+
print(f"❌ Failed to download diffx binary: {error}")
|
|
107
|
+
print("You may need to install diffx manually from: https://github.com/kako-jun/diffx/releases")
|
|
108
|
+
# Don't fail the installation, just warn
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
sys.exit(main())
|