jsonl-diff 0.1.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.
- jsonl_diff-0.1.0/LICENSE +21 -0
- jsonl_diff-0.1.0/PKG-INFO +548 -0
- jsonl_diff-0.1.0/README.md +494 -0
- jsonl_diff-0.1.0/jsonl_diff.egg-info/PKG-INFO +548 -0
- jsonl_diff-0.1.0/jsonl_diff.egg-info/SOURCES.txt +13 -0
- jsonl_diff-0.1.0/jsonl_diff.egg-info/dependency_links.txt +1 -0
- jsonl_diff-0.1.0/jsonl_diff.egg-info/entry_points.txt +2 -0
- jsonl_diff-0.1.0/jsonl_diff.egg-info/requires.txt +1 -0
- jsonl_diff-0.1.0/jsonl_diff.egg-info/top_level.txt +1 -0
- jsonl_diff-0.1.0/jsonl_diff.py +723 -0
- jsonl_diff-0.1.0/pyproject.toml +190 -0
- jsonl_diff-0.1.0/setup.cfg +4 -0
- jsonl_diff-0.1.0/tests/test_cli.py +309 -0
- jsonl_diff-0.1.0/tests/test_diff.py +506 -0
- jsonl_diff-0.1.0/tests/test_sources.py +156 -0
jsonl_diff-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rolando Morales Perez
|
|
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.
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jsonl-diff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Strict, disk-backed reconciliation of large JSONL/NDJSON datasets for Python.
|
|
5
|
+
Author-email: rmoralespp <rmoralespp@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Rolando Morales Perez
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Source, https://github.com/rmoralespp/jsonl-diff
|
|
29
|
+
Project-URL: Homepage, https://github.com/rmoralespp/jsonl-diff
|
|
30
|
+
Project-URL: Changelog, https://github.com/rmoralespp/jsonl-diff/blob/main/CHANGELOG.md
|
|
31
|
+
Project-URL: Issues, https://github.com/rmoralespp/jsonl-diff/issues
|
|
32
|
+
Project-URL: Documentation, https://rmoralespp.github.io/jsonl-diff/
|
|
33
|
+
Keywords: jsonlines,ndjson,jsonl,json,gzip,bzip2,xz,zst,utilities,serialization,deserialization,diff,json-diff,cli
|
|
34
|
+
Classifier: Programming Language :: Python
|
|
35
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
43
|
+
Classifier: Intended Audience :: Developers
|
|
44
|
+
Classifier: Intended Audience :: Information Technology
|
|
45
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
46
|
+
Classifier: Topic :: Internet
|
|
47
|
+
Classifier: Topic :: Utilities
|
|
48
|
+
Classifier: Topic :: File Formats :: JSON
|
|
49
|
+
Requires-Python: >=3.8
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
License-File: LICENSE
|
|
52
|
+
Requires-Dist: py-jsonl==1.5.2
|
|
53
|
+
Dynamic: license-file
|
|
54
|
+
|
|
55
|
+
# jsonl-diff
|
|
56
|
+
|
|
57
|
+
----
|
|
58
|
+
|
|
59
|
+
<p>
|
|
60
|
+
<img src="https://img.shields.io/pypi/v/jsonl-diff.svg" alt="PyPI">
|
|
61
|
+
<img src="https://img.shields.io/pypi/pyversions/jsonl-diff.svg" alt="Python">
|
|
62
|
+
<img src="https://github.com/rmoralespp/jsonl-diff/workflows/CI/badge.svg" alt="CI">
|
|
63
|
+
<img src="https://codecov.io/gh/rmoralespp/jsonl-diff/branch/main/graph/badge.svg" alt="Coverage">
|
|
64
|
+
<img src="https://img.shields.io/github/license/rmoralespp/jsonl-diff.svg" alt="License">
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
`jsonl-diff` performs strict, identity-based reconciliation of JSONL/NDJSON
|
|
68
|
+
datasets. It matches records by one or more top-level fields, then classifies
|
|
69
|
+
each identity as `equal`, `added`, `deleted`, or `modified`.
|
|
70
|
+
|
|
71
|
+
## Why jsonl-diff?
|
|
72
|
+
|
|
73
|
+
A text diff compares lines. That is usually the wrong model for datasets:
|
|
74
|
+
reordering unchanged records creates noise, and inserting one record can make
|
|
75
|
+
every later line appear different.
|
|
76
|
+
|
|
77
|
+
`jsonl-diff` fills the gap between line-oriented tools and in-memory dataframe comparisons:
|
|
78
|
+
|
|
79
|
+
- match records by identity, not physical position;
|
|
80
|
+
- compare datasets larger than available RAM using temporary SQLite storage;
|
|
81
|
+
- preserve original OLD and NEW line numbers for auditability;
|
|
82
|
+
- expose the same behavior through a CLI and a small Python API.
|
|
83
|
+
|
|
84
|
+
Use it for snapshots, ETL validation, migrations, exports, and CI checks. Use
|
|
85
|
+
`diff`, `jq`, or a visual JSON diff instead when you need line-level,
|
|
86
|
+
field-level, or JSON Patch output.
|
|
87
|
+
|
|
88
|
+
The comparison is backed by a temporary SQLite index. Complete inputs and
|
|
89
|
+
complete result sets do not need to fit in memory. Process memory is bounded by
|
|
90
|
+
the largest record plus parser and SQLite buffers, while indexed data lives on
|
|
91
|
+
temporary disk.
|
|
92
|
+
|
|
93
|
+
## Features
|
|
94
|
+
|
|
95
|
+
- Single or composite top-level identities, independent of input order.
|
|
96
|
+
- Configurable duplicate handling with strict failure by default.
|
|
97
|
+
- Exact RFC 6901 object-member ignores.
|
|
98
|
+
- Semantic number comparison without binary floating-point rounding.
|
|
99
|
+
- Deterministic summaries and changed-identity iteration.
|
|
100
|
+
- Original OLD and NEW physical line numbers for every change.
|
|
101
|
+
- Local, HTTP/HTTPS, file-like, and supported compressed sources through
|
|
102
|
+
`py-jsonl`.
|
|
103
|
+
- The same comparison engine through the CLI and Python API.
|
|
104
|
+
- Disk-backed comparison with configurable `jsonl-diff` temporary storage.
|
|
105
|
+
|
|
106
|
+
## Requirements and installation
|
|
107
|
+
|
|
108
|
+
`jsonl-diff` supports Python 3.8 through 3.14.
|
|
109
|
+
|
|
110
|
+
Install it in a project managed by `uv`:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv add jsonl-diff
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Install the CLI as a standalone tool:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uv tool install jsonl-diff
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`pip` is also supported:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python -m pip install jsonl-diff
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## CLI quick start
|
|
129
|
+
|
|
130
|
+
Given:
|
|
131
|
+
|
|
132
|
+
```jsonl
|
|
133
|
+
{"id":1,"name":"Ada"}
|
|
134
|
+
{"id":2,"name":"Grace"}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
and a reordered, changed dataset:
|
|
138
|
+
|
|
139
|
+
```jsonl
|
|
140
|
+
{"id":2,"name":"Grace Hopper"}
|
|
141
|
+
{"name":"Ada","id":1}
|
|
142
|
+
{"id":3,"name":"Linus"}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
compare them by `id`:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
jsonl-diff old.jsonl new.jsonl --key id
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Standard output contains only the text summary:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
Records:
|
|
155
|
+
equal: 1
|
|
156
|
+
added: 1
|
|
157
|
+
deleted: 0
|
|
158
|
+
modified: 1
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The result means one record is unchanged, one was added, and one existing
|
|
162
|
+
record changed. Physical order does not affect these counts.
|
|
163
|
+
|
|
164
|
+
Diagnostics are written to standard error. Use `--quiet` when only the exit
|
|
165
|
+
status or details file is needed.
|
|
166
|
+
|
|
167
|
+
### Common pipelines
|
|
168
|
+
|
|
169
|
+
Filter JSONL with `jq` before comparing it. `-c` keeps one compact JSON object
|
|
170
|
+
per line:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
jq -c 'select(has("deleted_at") and .deleted_at == null)' old.jsonl |
|
|
174
|
+
jsonl-diff - new.jsonl --key id
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Read compressed input directly when no preprocessing is required:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
jsonl-diff old.jsonl.xz new.jsonl.gz --key id
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
For two independent pipelines on Bash-compatible systems, use process
|
|
184
|
+
substitution:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
jsonl-diff \
|
|
188
|
+
<(head -n 1000 old.jsonl | jq -c 'select(.active)') \
|
|
189
|
+
<(head -n 1000 new.jsonl | jq -c 'select(.active)') \
|
|
190
|
+
--key id
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`-` represents stdin. Both inputs cannot use the same stdin stream.
|
|
194
|
+
|
|
195
|
+
### CLI syntax and options
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
jsonl-diff [-h] --key KEY [--ignore IGNORE]
|
|
199
|
+
[--duplicates {error,first,last}] [--details FILE] [--quiet]
|
|
200
|
+
[--max-temp MAX_TEMP]
|
|
201
|
+
old new
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| Argument | Meaning |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `old` | OLD local path, HTTP/HTTPS source, or `-` for stdin. |
|
|
207
|
+
| `new` | NEW local path, HTTP/HTTPS source, or `-` for stdin. |
|
|
208
|
+
| `--key KEY` | Required top-level identity field. Repeat it or use a comma-separated value for a composite identity. |
|
|
209
|
+
| `--ignore JSON_POINTER` | Exact RFC 6901 pointer to an object member to remove before content comparison. Repeatable. |
|
|
210
|
+
| `--duplicates POLICY` | Handle repeated identities with `error` (default), `first`, or `last`. |
|
|
211
|
+
| `--details FILE` | Write machine-readable JSONL details to `FILE`; details are never written to stdout. |
|
|
212
|
+
| `--quiet` | Suppress the normal stdout summary. Errors still go to stderr. |
|
|
213
|
+
| `--max-temp MAX_TEMP` | Limit storage owned by `jsonl-diff` to a positive integer number of bytes. |
|
|
214
|
+
| `-h`, `--help` | Show command help and exit. |
|
|
215
|
+
|
|
216
|
+
These composite-key forms are equivalent:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
jsonl-diff old.jsonl new.jsonl --key country,customerId,type
|
|
220
|
+
|
|
221
|
+
jsonl-diff old.jsonl new.jsonl \
|
|
222
|
+
--key country \
|
|
223
|
+
--key customerId \
|
|
224
|
+
--key type
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Ignore volatile fields by exact path:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
jsonl-diff old.jsonl new.jsonl \
|
|
231
|
+
--key id \
|
|
232
|
+
--ignore /updated_at \
|
|
233
|
+
--ignore /metadata/request_id
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Exit codes
|
|
237
|
+
|
|
238
|
+
| Code | Meaning |
|
|
239
|
+
|---:|---|
|
|
240
|
+
| `0` | The inputs are equal under the configured rules. |
|
|
241
|
+
| `1` | At least one added, deleted, or modified identity was found. |
|
|
242
|
+
| `2` | A handled input, source, resource, output, or interruption error occurred. |
|
|
243
|
+
| `3` | CLI usage or comparison configuration is invalid. |
|
|
244
|
+
|
|
245
|
+
Invalid argparse usage exits directly with code `3`. The Python API uses
|
|
246
|
+
exceptions instead of these process exit codes.
|
|
247
|
+
|
|
248
|
+
## Details JSONL
|
|
249
|
+
|
|
250
|
+
`--details FILE` streams a deterministic JSONL report through `py-jsonl` after
|
|
251
|
+
both inputs have been completely indexed and validated:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
jsonl-diff old.jsonl new.jsonl --key id --details changes.jsonl
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
For an OLD source containing identities `1` and `2`, and a NEW source
|
|
258
|
+
containing changed identity `2` and added identity `3`, the file is:
|
|
259
|
+
|
|
260
|
+
```jsonl
|
|
261
|
+
{"duplicates":"error","ignore":[],"key":["id"],"type":"meta"}
|
|
262
|
+
{"key":[1],"old_line":1,"op":"deleted","type":"change"}
|
|
263
|
+
{"key":[2],"new_line":1,"old_line":2,"op":"modified","type":"change"}
|
|
264
|
+
{"key":[3],"new_line":2,"op":"added","type":"change"}
|
|
265
|
+
{"added":1,"deleted":1,"equal":0,"modified":1,"type":"summary"}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Record types are:
|
|
269
|
+
|
|
270
|
+
- **`meta`**: always first. `key` contains normalized identity-field names;
|
|
271
|
+
`ignore` contains the configured ignore pointers.
|
|
272
|
+
- **`change`**: one per changed identity. `op` is `added`, `deleted`, or
|
|
273
|
+
`modified`. `old_line` is present for deletions and modifications;
|
|
274
|
+
`new_line` is present for additions and modifications. Equal records are not
|
|
275
|
+
emitted.
|
|
276
|
+
- **`summary`**: always last after a successful write, with all four totals.
|
|
277
|
+
|
|
278
|
+
The normal stdout summary is unchanged when `--details` is used, unless
|
|
279
|
+
`--quiet` suppresses it. A write failure can leave a partial details file and
|
|
280
|
+
returns exit code `2`.
|
|
281
|
+
|
|
282
|
+
## Python API
|
|
283
|
+
|
|
284
|
+
### Comparing sources
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from jsonl_diff import ChangeOperation, diff
|
|
288
|
+
|
|
289
|
+
with diff(
|
|
290
|
+
"old.jsonl.gz",
|
|
291
|
+
"new.jsonl.gz",
|
|
292
|
+
key=("country", "customerId"),
|
|
293
|
+
ignore=("/updated_at",),
|
|
294
|
+
duplicates="error",
|
|
295
|
+
max_temp=2_000_000_000,
|
|
296
|
+
) as result:
|
|
297
|
+
print(result.summary)
|
|
298
|
+
|
|
299
|
+
for change in result.changes(ChangeOperation.MODIFIED):
|
|
300
|
+
print(change.key, change.old_line, change.new_line)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The callable signature is:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from typing import Any, Optional, Sequence, Union
|
|
307
|
+
|
|
308
|
+
from jsonl_diff import DiffResult, DuplicatePolicy
|
|
309
|
+
|
|
310
|
+
def diff(
|
|
311
|
+
old: Any,
|
|
312
|
+
new: Any,
|
|
313
|
+
*,
|
|
314
|
+
key: Union[str, Sequence[str]],
|
|
315
|
+
ignore: Sequence[str] = (),
|
|
316
|
+
duplicates: Union[str, DuplicatePolicy] = DuplicatePolicy.ERROR,
|
|
317
|
+
max_temp: Optional[int] = None,
|
|
318
|
+
) -> DiffResult:
|
|
319
|
+
...
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`diff()` creates a comparison session without opening either source. Entering
|
|
323
|
+
its context fully reads, indexes, and validates both sources before exposing
|
|
324
|
+
the result. `DiffResult.changes()` is then a lazy iterator over the disk-backed
|
|
325
|
+
result rather than a list held in memory.
|
|
326
|
+
|
|
327
|
+
Using `DiffResult` as a context manager is required. It owns the temporary
|
|
328
|
+
resources and removes its private workspace on exit. A result cannot be entered
|
|
329
|
+
more than once. Its summary becomes available after entering and remains
|
|
330
|
+
available after closing; iterating changes requires the context to remain open.
|
|
331
|
+
|
|
332
|
+
### Result models
|
|
333
|
+
|
|
334
|
+
`result.summary` is an immutable `Summary`:
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
Summary(equal=10, added=2, deleted=1, modified=3)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
It exposes integer fields `equal`, `added`, `deleted`, and `modified`, plus the
|
|
341
|
+
boolean property `different`. The same values are available directly as
|
|
342
|
+
read-only `DiffResult` properties.
|
|
343
|
+
|
|
344
|
+
Each item from `result.changes()` is an immutable `Change` with:
|
|
345
|
+
|
|
346
|
+
- `operation`: a `ChangeOperation` enum value (`ADDED`, `DELETED`, or
|
|
347
|
+
`MODIFIED`);
|
|
348
|
+
- `key`: a typed tuple in normalized identity-field order;
|
|
349
|
+
- `old` and `new`: optional `SourceLocation` values;
|
|
350
|
+
- `old_line` and `new_line`: convenience properties returning a one-based
|
|
351
|
+
physical line or `None`.
|
|
352
|
+
|
|
353
|
+
For additions, `old`/`old_line` are `None`; for deletions,
|
|
354
|
+
`new`/`new_line` are `None`; modifications have both locations. Location
|
|
355
|
+
sources are labeled `OLD` and `NEW`.
|
|
356
|
+
|
|
357
|
+
Pass a `ChangeOperation` to filter without materializing all changes:
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
with diff("old.jsonl", "new.jsonl", key="id") as result:
|
|
361
|
+
added = result.changes(ChangeOperation.ADDED)
|
|
362
|
+
for change in added:
|
|
363
|
+
print(change.key, change.new_line)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
With numeric keys, API key components are `decimal.Decimal` values. For
|
|
367
|
+
example, JSON identity `7` is returned as `Decimal("7")`; JSON strings and
|
|
368
|
+
booleans retain their types. Details JSONL writes numeric keys as JSON numbers,
|
|
369
|
+
including arbitrary-precision integers.
|
|
370
|
+
|
|
371
|
+
### API errors
|
|
372
|
+
|
|
373
|
+
The public error hierarchy starts with `JsonlDiffError`:
|
|
374
|
+
|
|
375
|
+
- `ConfigurationError`: invalid keys, ignore pointers, or `max_temp`;
|
|
376
|
+
- `InputError`: an invalid source or record; exposes `source` and optional
|
|
377
|
+
`line`;
|
|
378
|
+
- `DuplicateKeyError`: an `InputError` with `key` and the first/repeated
|
|
379
|
+
physical lines in `lines`;
|
|
380
|
+
- `ResourceError`: the temporary index cannot be created, written, or kept
|
|
381
|
+
within its configured limit.
|
|
382
|
+
|
|
383
|
+
Failures during `diff()` clean up the workspace before the exception is
|
|
384
|
+
raised.
|
|
385
|
+
|
|
386
|
+
## Sources and compression
|
|
387
|
+
|
|
388
|
+
Source opening and decompression are delegated to `py-jsonl`:
|
|
389
|
+
|
|
390
|
+
| Source | CLI | Python API |
|
|
391
|
+
|---|:---:|:---:|
|
|
392
|
+
| Local path | Yes | Yes, including path-like values |
|
|
393
|
+
| HTTP/HTTPS URL | Yes | Yes |
|
|
394
|
+
| File-like object | No | Yes |
|
|
395
|
+
| gzip (`.gz`) | Yes | Yes |
|
|
396
|
+
| bzip2 (`.bz2`) | Yes | Yes |
|
|
397
|
+
| xz (`.xz`) | Yes | Yes |
|
|
398
|
+
| Zstandard (`.zst`) | Python 3.14 only | Python 3.14 only |
|
|
399
|
+
| ZIP archive | No | No |
|
|
400
|
+
|
|
401
|
+
Zstandard availability follows `py-jsonl` and its use of Python 3.14's
|
|
402
|
+
standard-library zstd support; it is not supported by this project on earlier
|
|
403
|
+
Python versions. Compression and HTTP transfer boundaries do not affect the
|
|
404
|
+
reported decoded JSONL line numbers.
|
|
405
|
+
|
|
406
|
+
## Comparison semantics
|
|
407
|
+
|
|
408
|
+
### Strict JSONL records
|
|
409
|
+
|
|
410
|
+
- Every physical line must be non-blank, valid JSON containing exactly one
|
|
411
|
+
top-level object.
|
|
412
|
+
- Local paths and binary streams are decoded as UTF-8. Text streams are
|
|
413
|
+
already decoded, while URL response charset handling follows `py-jsonl`
|
|
414
|
+
(defaulting to UTF-8 when no charset is declared).
|
|
415
|
+
- Blank lines, malformed JSON, duplicate object property names, `NaN`,
|
|
416
|
+
`Infinity`, and `-Infinity` are errors.
|
|
417
|
+
- Top-level arrays, strings, numbers, booleans, and `null` are rejected.
|
|
418
|
+
- Input errors identify `OLD` or `NEW` and include the physical line when
|
|
419
|
+
available.
|
|
420
|
+
|
|
421
|
+
No normal summary or details output begins until both inputs pass complete
|
|
422
|
+
input and duplicate validation.
|
|
423
|
+
|
|
424
|
+
### Identity and duplicates
|
|
425
|
+
|
|
426
|
+
Identity fields are top-level object-member names. Each component must exist
|
|
427
|
+
and be a non-null string, number, or boolean; objects and arrays are invalid.
|
|
428
|
+
Nested identity paths and automatic key detection are not supported.
|
|
429
|
+
|
|
430
|
+
Composite identity field names are sorted lexically before their values are
|
|
431
|
+
extracted. Consequently, `key=("b", "a")` and `key=("a", "b")` both produce
|
|
432
|
+
keys in `(a, b)` order and match identically. Repeated or empty identity-field
|
|
433
|
+
names are configuration errors.
|
|
434
|
+
|
|
435
|
+
JSON types remain significant: `"1"` is different from `1`, and `true` is
|
|
436
|
+
different from `1`.
|
|
437
|
+
|
|
438
|
+
Every normalized identity must be unique within OLD and within NEW by default.
|
|
439
|
+
The first duplicate aborts the comparison; `DuplicateKeyError` reports the
|
|
440
|
+
first occurrence and repeated physical line. Set `duplicates="first"` to keep
|
|
441
|
+
the first occurrence or `duplicates="last"` to keep the last occurrence and
|
|
442
|
+
its physical line. These tolerant policies depend on physical input order.
|
|
443
|
+
|
|
444
|
+
### Ignored object members
|
|
445
|
+
|
|
446
|
+
Ignore expressions are exact
|
|
447
|
+
[RFC 6901 JSON Pointers](https://www.rfc-editor.org/rfc/rfc6901). Escapes such
|
|
448
|
+
as `~1` for `/` and `~0` for `~` are supported. An absent path is harmless,
|
|
449
|
+
and `/updated_at` does not match `/metadata/updated_at`.
|
|
450
|
+
|
|
451
|
+
Ignores are applied symmetrically before content canonicalization. A pointer
|
|
452
|
+
may remove an entire array-valued object member, but it may not traverse an
|
|
453
|
+
array or address an array element. Ignoring a top-level identity field is also
|
|
454
|
+
invalid. Wildcards, JSONPath, and recursive name matching are not supported.
|
|
455
|
+
|
|
456
|
+
### Canonical content and numbers
|
|
457
|
+
|
|
458
|
+
Records with the same identity are compared after ignored members are removed:
|
|
459
|
+
|
|
460
|
+
- JSON object property order is insignificant at every depth.
|
|
461
|
+
- Array order is significant.
|
|
462
|
+
- Unicode strings are compared by code-point sequence; no Unicode
|
|
463
|
+
normalization is applied.
|
|
464
|
+
- Numbers are parsed as `Decimal` and compared by mathematical value.
|
|
465
|
+
`1`, `1.0`, and `1e0` are equal, and negative zero equals zero.
|
|
466
|
+
- Arbitrary-precision integers and decimal values are not rounded through
|
|
467
|
+
binary floating point.
|
|
468
|
+
|
|
469
|
+
Canonical content is stored with its length and SHA-256 digest. Matching
|
|
470
|
+
canonical bytes are still compared, so a digest match alone is not treated as
|
|
471
|
+
proof of equality. Numbers are written using compact scientific notation; a
|
|
472
|
+
large exponent does not expand into a large string of zeroes.
|
|
473
|
+
|
|
474
|
+
### Determinism
|
|
475
|
+
|
|
476
|
+
Summary counts do not depend on either source's physical record order.
|
|
477
|
+
Changes, including details records, use the stable lexical byte order of each
|
|
478
|
+
canonical typed identity. This is a reproducibility guarantee, not numeric,
|
|
479
|
+
locale-aware, or human-oriented sorting.
|
|
480
|
+
|
|
481
|
+
## Disk-backed architecture and temporary files
|
|
482
|
+
|
|
483
|
+
Each record is parsed and validated incrementally, normalized, and inserted
|
|
484
|
+
into a private SQLite database under an operating-system temporary directory.
|
|
485
|
+
The database stores the typed canonical identity, original line, canonical
|
|
486
|
+
content, content length, and digest. A uniqueness constraint detects duplicate
|
|
487
|
+
identities. SQL joins calculate the summary, and ordered SQLite cursors drive
|
|
488
|
+
lazy change iteration.
|
|
489
|
+
|
|
490
|
+
This architecture bounds memory by the records currently being processed and
|
|
491
|
+
database buffers; it does not keep the complete decoded inputs or all changes
|
|
492
|
+
in RAM. It does require temporary disk space. Canonical records and SQLite
|
|
493
|
+
indexes remain until the `DiffResult` is closed, so temporary usage can exceed
|
|
494
|
+
twice the combined decompressed input size.
|
|
495
|
+
|
|
496
|
+
`max_temp` / `--max-temp` accepts a positive byte count. The implementation
|
|
497
|
+
limits and checks files in the workspace owned by `jsonl-diff`, raising
|
|
498
|
+
`ResourceError` (CLI exit `2`) when the index cannot stay within that budget.
|
|
499
|
+
Choose a limit with room for SQLite pages and index overhead.
|
|
500
|
+
|
|
501
|
+
`py-jsonl` may create its own temporary staging files for remote or compressed
|
|
502
|
+
sources. Those files follow `py-jsonl`'s resource policy and are not counted by
|
|
503
|
+
`jsonl-diff`'s `max_temp` limit. Total system temporary usage can therefore
|
|
504
|
+
exceed the configured value.
|
|
505
|
+
|
|
506
|
+
The private workspace is removed on context-manager exit, explicit `close()`,
|
|
507
|
+
or a handled failure during construction. Cleanup failures are emitted as
|
|
508
|
+
warnings rather than replacing the primary error.
|
|
509
|
+
|
|
510
|
+
## Limitations and non-goals
|
|
511
|
+
|
|
512
|
+
`jsonl-diff` intentionally does not provide:
|
|
513
|
+
|
|
514
|
+
- line- or physical-order comparison;
|
|
515
|
+
- equal-record events or complete record payloads in details output;
|
|
516
|
+
- field-level diffs, JSON Patch, move detection, or rename detection;
|
|
517
|
+
- nested identities, fuzzy matching, or key autodetection;
|
|
518
|
+
- numeric tolerances, unordered-array comparison, or Unicode normalization;
|
|
519
|
+
- wildcard/JSONPath ignores or ignores that traverse arrays;
|
|
520
|
+
- schema validation or repair of malformed input;
|
|
521
|
+
- ZIP input, database-table input, cloud-provider SDK integrations, GUI, or
|
|
522
|
+
HTML reports.
|
|
523
|
+
|
|
524
|
+
It is a reconciliation tool for strict JSONL datasets, not a general-purpose
|
|
525
|
+
visual JSON diff.
|
|
526
|
+
|
|
527
|
+
## Development
|
|
528
|
+
|
|
529
|
+
Clone the repository and sync the test and lint dependency groups with `uv`:
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
uv sync --group test --group lint
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Run the test suite and Ruff:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
uv run pytest
|
|
539
|
+
uv run ruff check --quiet --output-format=concise .
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
The tests cover CLI behavior, identity and canonicalization rules, strict
|
|
543
|
+
validation, details output, source types, compression, temporary limits, and
|
|
544
|
+
resource cleanup.
|
|
545
|
+
|
|
546
|
+
## License
|
|
547
|
+
|
|
548
|
+
See [LICENSE](LICENSE).
|