pyapi-diff 0.0.1__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.
- pyapi/__init__.py +4 -0
- pyapi/comparator.py +101 -0
- pyapi/models.py +20 -0
- pyapi_diff-0.0.1.dist-info/METADATA +168 -0
- pyapi_diff-0.0.1.dist-info/RECORD +7 -0
- pyapi_diff-0.0.1.dist-info/WHEEL +4 -0
- pyapi_diff-0.0.1.dist-info/licenses/LICENSE +21 -0
pyapi/__init__.py
ADDED
pyapi/comparator.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from pyapi.models import Change
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
def _json_type(value: any) -> any:
|
|
6
|
+
|
|
7
|
+
if value is None:
|
|
8
|
+
return "null"
|
|
9
|
+
if isinstance(value, bool):
|
|
10
|
+
return "boolean"
|
|
11
|
+
if isinstance(value, int):
|
|
12
|
+
return "number"
|
|
13
|
+
if isinstance(value, str):
|
|
14
|
+
return "string"
|
|
15
|
+
if isinstance(value, dict):
|
|
16
|
+
return "object"
|
|
17
|
+
if isinstance(value, list):
|
|
18
|
+
return "array"
|
|
19
|
+
return "Unknown"
|
|
20
|
+
|
|
21
|
+
def _compare(old: dict, new: dict, path: str, changes: list)-> list:
|
|
22
|
+
|
|
23
|
+
old_type: any = _json_type(old)
|
|
24
|
+
new_type: any = _json_type(new)
|
|
25
|
+
|
|
26
|
+
if old_type != new_type:
|
|
27
|
+
changes.append(
|
|
28
|
+
Change(
|
|
29
|
+
kind= "type_changed",
|
|
30
|
+
path= path or "$",
|
|
31
|
+
old_value= old_type,
|
|
32
|
+
new_value= new_type
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if isinstance(old, dict):
|
|
38
|
+
old_keys: list = set(old)
|
|
39
|
+
new_keys: list = set(new)
|
|
40
|
+
|
|
41
|
+
for key in sorted(old_keys - new_keys):
|
|
42
|
+
changes.append(
|
|
43
|
+
Change(
|
|
44
|
+
kind="key_removed",
|
|
45
|
+
path= f"{path}.{key}",
|
|
46
|
+
old_value=old[key]
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
for key in sorted(new_keys - old_keys):
|
|
51
|
+
changes.append(
|
|
52
|
+
Change(
|
|
53
|
+
kind= "key_added",
|
|
54
|
+
path= f"{path}.{key}",
|
|
55
|
+
new_value= new[key]
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
for key in sorted(old_keys & new_keys):
|
|
60
|
+
_compare(
|
|
61
|
+
old[key],
|
|
62
|
+
new[key],
|
|
63
|
+
f"{path}.{key}",
|
|
64
|
+
changes
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
elif isinstance(old, list):
|
|
68
|
+
for index, (old_item, new_item) in enumerate(zip(old, new)):
|
|
69
|
+
_compare(
|
|
70
|
+
old_item,
|
|
71
|
+
new_item,
|
|
72
|
+
f"{path}[{index}]",
|
|
73
|
+
changes
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def json_compare(old: dict, new: dict) -> list:
|
|
77
|
+
old_path: str = Path(old)
|
|
78
|
+
new_path: str = Path(new)
|
|
79
|
+
|
|
80
|
+
with old_path.open(encoding="utf-8") as f:
|
|
81
|
+
old_data: dict = json.load(f)
|
|
82
|
+
|
|
83
|
+
with new_path.open(encoding="utf-8") as f:
|
|
84
|
+
new_data: dict = json.load(f)
|
|
85
|
+
|
|
86
|
+
changes: list= []
|
|
87
|
+
_compare(
|
|
88
|
+
old_data,
|
|
89
|
+
new_data,
|
|
90
|
+
"$",
|
|
91
|
+
changes
|
|
92
|
+
)
|
|
93
|
+
return changes
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
pyapi/models.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
@dataclass(frozen=True)
|
|
4
|
+
class Change:
|
|
5
|
+
kind:str
|
|
6
|
+
path: str
|
|
7
|
+
old_value: object = None
|
|
8
|
+
new_value: object = None
|
|
9
|
+
|
|
10
|
+
def __str__(self)-> str:
|
|
11
|
+
"""
|
|
12
|
+
This magical method will allow to represent the data in format.
|
|
13
|
+
:return: data in format
|
|
14
|
+
:rtype: str
|
|
15
|
+
"""
|
|
16
|
+
return (
|
|
17
|
+
f"{self.kind}: {self.path} "
|
|
18
|
+
f"(old = {self.old_value!r}, "
|
|
19
|
+
f"new = {self.new_value!r})"
|
|
20
|
+
)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyapi-diff
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: python tool to indentify breaking changes in apis.
|
|
5
|
+
Author-email: Prashant Sharma <prashant.ps9833@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: comparison,developer-tools,json,json-diff
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# pyapi
|
|
17
|
+
|
|
18
|
+
**A lightweight Python library to detect structural changes in JSON data.**
|
|
19
|
+
|
|
20
|
+
`pyapi` compares two JSON-compatible data structures and identifies changes in their structure and data types, without reporting ordinary value changes.
|
|
21
|
+
|
|
22
|
+
It is useful for detecting changes in API response structures, checking compatibility between API versions, and identifying unexpected changes in JSON data.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Key Addition Detection** — Identifies keys present in the new structure but missing from the old structure.
|
|
27
|
+
- **Key Removal Detection** — Identifies keys present in the old structure but missing from the new structure.
|
|
28
|
+
- **Type Change Detection** — Detects when a value's data type changes between two structures.
|
|
29
|
+
- **Nested Structure Comparison** — Detects structural changes within nested JSON objects.
|
|
30
|
+
- **Value-Agnostic Comparison** — Ignores changes to values when their data types remain the same.
|
|
31
|
+
- **Readable Change Paths** — Reports the location of each detected change using JSON-style paths.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install pyapi
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pyapi import json_compare
|
|
43
|
+
|
|
44
|
+
old = {
|
|
45
|
+
"id": 101,
|
|
46
|
+
"name": "abc",
|
|
47
|
+
"age": 55,
|
|
48
|
+
"address": {
|
|
49
|
+
"city": "san francisco",
|
|
50
|
+
"pincode": 12345
|
|
51
|
+
},
|
|
52
|
+
"skills": ["Python", "SQL"]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
new = {
|
|
56
|
+
"id": "101",
|
|
57
|
+
"name": "xyz",
|
|
58
|
+
"contact": 1234567890,
|
|
59
|
+
"address": {
|
|
60
|
+
"city": "Seattle",
|
|
61
|
+
"pincode": 1234
|
|
62
|
+
},
|
|
63
|
+
"skills": ["Python", "SQL"]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
json_compare(old, new)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Output
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
key_removed: $.age (old = 55, new = None)
|
|
73
|
+
key_added: $.contact (old = None, new = 1234567890)
|
|
74
|
+
type_changed: $.id (old = 'number', new = 'string')
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Detected Change Types
|
|
78
|
+
|
|
79
|
+
| Change Type | Description |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `key_added` | A key exists in the new structure but not in the old structure. |
|
|
82
|
+
| `key_removed` | A key exists in the old structure but not in the new structure. |
|
|
83
|
+
| `type_changed` | A value's data type differs between the old and new structures. |
|
|
84
|
+
|
|
85
|
+
## How It Works
|
|
86
|
+
|
|
87
|
+
`pyapi` compares an old JSON-compatible structure against a new one and reports structural differences.
|
|
88
|
+
|
|
89
|
+
For example, consider these two objects:
|
|
90
|
+
|
|
91
|
+
**Old structure**
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"user": {
|
|
96
|
+
"id": 101,
|
|
97
|
+
"name": "Alice"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**New structure**
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"user": {
|
|
107
|
+
"id": "101",
|
|
108
|
+
"name": "Bob",
|
|
109
|
+
"email": "alice@example.com"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The detected changes include:
|
|
115
|
+
|
|
116
|
+
- `type_changed` at `$.user.id`, because `id` changes from a number to a string.
|
|
117
|
+
- `key_added` at `$.user.email`, because `email` is newly introduced.
|
|
118
|
+
|
|
119
|
+
The change from `"Alice"` to `"Bob"` is ignored because the value remains a string.
|
|
120
|
+
|
|
121
|
+
## Use Cases
|
|
122
|
+
|
|
123
|
+
- **API Response Comparison:** Identify structural changes between responses from different API versions.
|
|
124
|
+
- **Breaking Change Detection:** Find removed keys or changed data types that could break API consumers.
|
|
125
|
+
- **Regression Testing:** Detect unexpected changes in JSON response structures during development.
|
|
126
|
+
- **Data Validation:** Check whether incoming JSON data maintains the expected structure.
|
|
127
|
+
- **Configuration Comparison:** Identify added or removed settings and changes in their data types.
|
|
128
|
+
|
|
129
|
+
## Requirements
|
|
130
|
+
|
|
131
|
+
- Python 3.10 or later
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
Clone the repository:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
git clone YOUR_REPOSITORY_URL
|
|
139
|
+
cd pyapi
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Install dependencies using [uv](https://docs.astral.sh/uv/):
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uv sync
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Run the example or test script:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
uv run python tests/test1.py
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Project Status
|
|
155
|
+
|
|
156
|
+
`pyapi` is currently in its initial release, `0.0.1`.
|
|
157
|
+
|
|
158
|
+
The current focus is on detecting JSON structural and data type changes. Additional functionality may be introduced in future releases.
|
|
159
|
+
|
|
160
|
+
## Contributing
|
|
161
|
+
|
|
162
|
+
Contributions, bug reports, and feature suggestions are welcome.
|
|
163
|
+
|
|
164
|
+
Feel free to open an issue or submit a pull request in the project's GitHub repository.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pyapi/__init__.py,sha256=TbYWYNFAFsIaxtiHBLqpI_cxsTleRY1-W3Dha4IzPF8,101
|
|
2
|
+
pyapi/comparator.py,sha256=HwkykHy9xJFwKnpKqlJdoytoTyHjEmi3KyS5CqbRwzM,2360
|
|
3
|
+
pyapi/models.py,sha256=RvdW6MI-IK8UVYuyEAcICK-Mn5rwg0-M-3jUvhXbbAQ,487
|
|
4
|
+
pyapi_diff-0.0.1.dist-info/METADATA,sha256=of2bYWDdSEopfLNY9lGZ-mkw_MBvtl-srInnCu0-8EM,4452
|
|
5
|
+
pyapi_diff-0.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
pyapi_diff-0.0.1.dist-info/licenses/LICENSE,sha256=LQrD-qdW6uCj3h5BBAk0C5MVrvuiYDBBjxJAM8BE1_k,1072
|
|
7
|
+
pyapi_diff-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Prashant Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|