jsonl-diff 0.1.0__tar.gz → 0.1.1__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.
@@ -0,0 +1,297 @@
1
+ Metadata-Version: 2.4
2
+ Name: jsonl-diff
3
+ Version: 0.1.1
4
+ Summary: A lightweight, pure-Python tool for comparing large JSONL/NDJSON datasets without loading them entirely into memory.
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
+ Requires-Dist: jmespath>=1.0.0
54
+ Dynamic: license-file
55
+
56
+ # jsonl-diff
57
+
58
+ <p>
59
+ <img src="https://img.shields.io/pypi/v/jsonl-diff.svg" alt="PyPI">
60
+ <img src="https://img.shields.io/pypi/pyversions/jsonl-diff.svg" alt="Python">
61
+ <img src="https://github.com/rmoralespp/jsonl-diff/workflows/CI/badge.svg" alt="CI">
62
+ <img src="https://codecov.io/gh/rmoralespp/jsonl-diff/branch/main/graph/badge.svg" alt="Coverage">
63
+ <img src="https://img.shields.io/github/license/rmoralespp/jsonl-diff.svg" alt="License">
64
+ </p>
65
+
66
+ Compare large JSONL/NDJSON datasets **by record identity instead of line
67
+ position**, without loading the complete inputs into memory.
68
+
69
+ `jsonl-diff` matches records using one or more top-level fields and reports:
70
+
71
+ * `equal`
72
+ * `added`
73
+ * `deleted`
74
+ * `modified`
75
+
76
+ It is useful for snapshots, ETL validation, migrations, exports, and CI checks.
77
+
78
+ > For line-level, field-level, JSON Patch, or visual diffs, use a tool designed for those purposes instead.
79
+
80
+ ## Installation
81
+
82
+ Python 3.8–3.14.
83
+
84
+ ```bash
85
+ python -m pip install jsonl-diff
86
+ ```
87
+
88
+ ## Quick start
89
+
90
+ Given:
91
+
92
+ ```jsonl
93
+ {"id":1,"name":"Ada"}
94
+ {"id":2,"name":"Grace"}
95
+ ```
96
+
97
+ and:
98
+
99
+ ```jsonl
100
+ {"id":2,"name":"Grace Hopper"}
101
+ {"name":"Ada","id":1}
102
+ {"id":3,"name":"Linus"}
103
+ ```
104
+
105
+ compare them by `id`:
106
+
107
+ ```bash
108
+ jsonl-diff old.jsonl new.jsonl --key id
109
+ ```
110
+
111
+ ```text
112
+ Records:
113
+ equal: 1
114
+ added: 1
115
+ deleted: 0
116
+ modified: 1
117
+ ```
118
+
119
+ Record order does not matter.
120
+
121
+ ## Features
122
+
123
+ * Single or composite top-level identities.
124
+ * Disk-backed comparison for large datasets ([architecture](docs/architecture.md)).
125
+ * Optional JMESPath filtering with `--where`.
126
+ * Configurable duplicate handling: `error`, `first`, or `last`.
127
+ * Exact RFC 6901 JSON Pointer ignores.
128
+ * Semantic number comparison using `Decimal`.
129
+ * Deterministic summaries and change iteration.
130
+ * Original OLD/NEW physical line numbers.
131
+ * Machine-readable JSONL change log with `--details` ([format](docs/details-format.md)).
132
+ * Local, HTTP/HTTPS, file-like, and supported compressed sources.
133
+ * CLI and Python API using the same comparison engine.
134
+
135
+ ## CLI
136
+
137
+ ```text
138
+ jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
139
+ [--duplicates {error,first,last}] [--details FILE] [--quiet]
140
+ [--max-temp MAX_TEMP]
141
+ old new
142
+ ```
143
+
144
+ | Option | Description |
145
+ | --------------------- | ------------------------------------------------------------------------------ |
146
+ | `old`, `new` | Local path, HTTP/HTTPS source, or `-` for stdin |
147
+ | `--key KEY` | Required top-level identity field; repeat or comma-separate for composite keys |
148
+ | `--ignore POINTER` | RFC 6901 pointer to exclude from content comparison |
149
+ | `--where EXPRESSION` | JMESPath filter applied to each record |
150
+ | `--duplicates POLICY` | `error` (default), `first`, or `last` |
151
+ | `--details FILE` | Write deterministic machine-readable JSONL changes |
152
+ | `--quiet` | Suppress the normal summary |
153
+ | `--max-temp BYTES` | Limit `jsonl-diff` temporary storage |
154
+
155
+ Examples:
156
+
157
+ ```bash
158
+ # Composite identity
159
+ jsonl-diff old.jsonl new.jsonl --key country,customerId,type
160
+
161
+ # Ignore volatile fields
162
+ jsonl-diff old.jsonl new.jsonl \
163
+ --key id \
164
+ --ignore /updated_at \
165
+ --ignore /metadata/request_id
166
+
167
+ # Filter records
168
+ jsonl-diff old.jsonl new.jsonl \
169
+ --key id \
170
+ --where 'deleted_at == `null`'
171
+
172
+ # Compressed input
173
+ jsonl-diff old.jsonl.xz new.jsonl.gz --key id
174
+ ```
175
+
176
+ ### Exit codes
177
+
178
+ | Code | Meaning |
179
+ | ---: | ----------------------------------------- |
180
+ | `0` | Inputs are equal |
181
+ | `1` | Added, deleted, or modified records found |
182
+ | `2` | Input, resource, output, or runtime error |
183
+ | `3` | Invalid CLI configuration or usage |
184
+
185
+ ## Python API
186
+
187
+ ```python
188
+ from jsonl_diff import ChangeOperation, diff
189
+
190
+ with diff(
191
+ "old.jsonl.gz",
192
+ "new.jsonl.gz",
193
+ key=("country", "customerId"),
194
+ ignore=("/updated_at",),
195
+ where='country == `"ES"`',
196
+ ) as result:
197
+ print(result.summary)
198
+
199
+ for change in result.changes(ChangeOperation.MODIFIED):
200
+ print(change.key, change.old_line, change.new_line)
201
+ ```
202
+
203
+ `diff()` returns a disk-backed `DiffResult`, used as a context manager.
204
+ Results are streamed lazily through `changes()` rather than materialized in
205
+ memory.
206
+
207
+ The main result models are:
208
+
209
+ ```python
210
+ Summary(equal=10, added=2, deleted=1, modified=3)
211
+ ```
212
+
213
+ and immutable `Change` objects containing the operation, typed identity, and
214
+ OLD/NEW source locations.
215
+
216
+ The public exception hierarchy is rooted at `JsonlDiffError`:
217
+
218
+ * `ConfigurationError`
219
+ * `InputError`
220
+ * `DuplicateKeyError`
221
+ * `ResourceError`
222
+
223
+ See the [Python API reference](docs/python-api.md) for the full callable
224
+ signature, result models, `Decimal` key semantics, and error hierarchy.
225
+
226
+ ## Comparison semantics
227
+
228
+ `jsonl-diff` is intentionally strict:
229
+
230
+ * Every input line must contain exactly one valid top-level JSON object.
231
+ * Blank lines, malformed JSON, duplicate properties, `NaN`, and infinities are rejected.
232
+ * Identity fields must be top-level scalar values (`null` allowed only as one component of a composite key).
233
+ * Identity types remain significant: `"1"` ≠ `1`, `true` ≠ `1`.
234
+ * Duplicate identities fail by default; `--duplicates first`/`last` tolerate them.
235
+ * Object property order is ignored; array order is significant.
236
+ * Numbers are compared by mathematical value: `1`, `1.0`, and `1e0` are equal.
237
+ * Unicode strings are compared without normalization.
238
+ * Changes are reported in deterministic identity order.
239
+
240
+ `--where` selects which records participate; `--key` defines identity;
241
+ `--ignore` removes fields from content comparison. See
242
+ [Comparison semantics](docs/comparison-semantics.md) for the full rules,
243
+ including duplicate handling, ignore-pointer edge cases, `--where` evaluation
244
+ order, and canonical number formatting.
245
+
246
+ ## Sources & compression
247
+
248
+ Supported sources include local paths and HTTP/HTTPS URLs. The Python API
249
+ also accepts file-like objects.
250
+
251
+ Supported compression:
252
+
253
+ * gzip
254
+ * bzip2
255
+ * xz
256
+ * Zstandard on Python 3.14
257
+
258
+ See [architecture](docs/architecture.md) for the full source/compression
259
+ support matrix and how sources are delegated to `py-jsonl`.
260
+
261
+ ## Limitations
262
+
263
+ `jsonl-diff` does **not** provide:
264
+
265
+ * line/position-based diffs
266
+ * field-level diffs or JSON Patch
267
+ * move/rename detection
268
+ * nested identities or automatic key detection
269
+ * fuzzy matching or numeric tolerances
270
+ * unordered-array comparison
271
+ * schema validation or input repair
272
+ * ZIP, database, or cloud-provider inputs
273
+ * GUI or HTML reports
274
+
275
+ It is a **dataset reconciliation tool**, not a general-purpose visual JSON diff.
276
+
277
+ ## Further reading
278
+
279
+ * [Comparison semantics](docs/comparison-semantics.md) — identity, duplicates, ignores, `--where`, canonical numbers, determinism.
280
+ * [Disk-backed architecture](docs/architecture.md) — SQLite index, `max_temp`, cleanup, sources and compression.
281
+ * [Details JSONL format](docs/details-format.md) — machine-readable `--details` output schema.
282
+ * [Python API reference](docs/python-api.md) — full signature, result models, and error hierarchy.
283
+
284
+ ## Development
285
+
286
+ ```bash
287
+ uv sync --group test --group lint
288
+
289
+ uv run pytest
290
+ uv run ruff check --quiet --output-format=concise .
291
+ ```
292
+
293
+ The test suite covers CLI behavior, identity/canonicalization, validation, details output, sources, compression, temporary limits, and cleanup.
294
+
295
+ ## License
296
+
297
+ See [LICENSE](LICENSE).
@@ -0,0 +1,242 @@
1
+ # jsonl-diff
2
+
3
+ <p>
4
+ <img src="https://img.shields.io/pypi/v/jsonl-diff.svg" alt="PyPI">
5
+ <img src="https://img.shields.io/pypi/pyversions/jsonl-diff.svg" alt="Python">
6
+ <img src="https://github.com/rmoralespp/jsonl-diff/workflows/CI/badge.svg" alt="CI">
7
+ <img src="https://codecov.io/gh/rmoralespp/jsonl-diff/branch/main/graph/badge.svg" alt="Coverage">
8
+ <img src="https://img.shields.io/github/license/rmoralespp/jsonl-diff.svg" alt="License">
9
+ </p>
10
+
11
+ Compare large JSONL/NDJSON datasets **by record identity instead of line
12
+ position**, without loading the complete inputs into memory.
13
+
14
+ `jsonl-diff` matches records using one or more top-level fields and reports:
15
+
16
+ * `equal`
17
+ * `added`
18
+ * `deleted`
19
+ * `modified`
20
+
21
+ It is useful for snapshots, ETL validation, migrations, exports, and CI checks.
22
+
23
+ > For line-level, field-level, JSON Patch, or visual diffs, use a tool designed for those purposes instead.
24
+
25
+ ## Installation
26
+
27
+ Python 3.8–3.14.
28
+
29
+ ```bash
30
+ python -m pip install jsonl-diff
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ Given:
36
+
37
+ ```jsonl
38
+ {"id":1,"name":"Ada"}
39
+ {"id":2,"name":"Grace"}
40
+ ```
41
+
42
+ and:
43
+
44
+ ```jsonl
45
+ {"id":2,"name":"Grace Hopper"}
46
+ {"name":"Ada","id":1}
47
+ {"id":3,"name":"Linus"}
48
+ ```
49
+
50
+ compare them by `id`:
51
+
52
+ ```bash
53
+ jsonl-diff old.jsonl new.jsonl --key id
54
+ ```
55
+
56
+ ```text
57
+ Records:
58
+ equal: 1
59
+ added: 1
60
+ deleted: 0
61
+ modified: 1
62
+ ```
63
+
64
+ Record order does not matter.
65
+
66
+ ## Features
67
+
68
+ * Single or composite top-level identities.
69
+ * Disk-backed comparison for large datasets ([architecture](docs/architecture.md)).
70
+ * Optional JMESPath filtering with `--where`.
71
+ * Configurable duplicate handling: `error`, `first`, or `last`.
72
+ * Exact RFC 6901 JSON Pointer ignores.
73
+ * Semantic number comparison using `Decimal`.
74
+ * Deterministic summaries and change iteration.
75
+ * Original OLD/NEW physical line numbers.
76
+ * Machine-readable JSONL change log with `--details` ([format](docs/details-format.md)).
77
+ * Local, HTTP/HTTPS, file-like, and supported compressed sources.
78
+ * CLI and Python API using the same comparison engine.
79
+
80
+ ## CLI
81
+
82
+ ```text
83
+ jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
84
+ [--duplicates {error,first,last}] [--details FILE] [--quiet]
85
+ [--max-temp MAX_TEMP]
86
+ old new
87
+ ```
88
+
89
+ | Option | Description |
90
+ | --------------------- | ------------------------------------------------------------------------------ |
91
+ | `old`, `new` | Local path, HTTP/HTTPS source, or `-` for stdin |
92
+ | `--key KEY` | Required top-level identity field; repeat or comma-separate for composite keys |
93
+ | `--ignore POINTER` | RFC 6901 pointer to exclude from content comparison |
94
+ | `--where EXPRESSION` | JMESPath filter applied to each record |
95
+ | `--duplicates POLICY` | `error` (default), `first`, or `last` |
96
+ | `--details FILE` | Write deterministic machine-readable JSONL changes |
97
+ | `--quiet` | Suppress the normal summary |
98
+ | `--max-temp BYTES` | Limit `jsonl-diff` temporary storage |
99
+
100
+ Examples:
101
+
102
+ ```bash
103
+ # Composite identity
104
+ jsonl-diff old.jsonl new.jsonl --key country,customerId,type
105
+
106
+ # Ignore volatile fields
107
+ jsonl-diff old.jsonl new.jsonl \
108
+ --key id \
109
+ --ignore /updated_at \
110
+ --ignore /metadata/request_id
111
+
112
+ # Filter records
113
+ jsonl-diff old.jsonl new.jsonl \
114
+ --key id \
115
+ --where 'deleted_at == `null`'
116
+
117
+ # Compressed input
118
+ jsonl-diff old.jsonl.xz new.jsonl.gz --key id
119
+ ```
120
+
121
+ ### Exit codes
122
+
123
+ | Code | Meaning |
124
+ | ---: | ----------------------------------------- |
125
+ | `0` | Inputs are equal |
126
+ | `1` | Added, deleted, or modified records found |
127
+ | `2` | Input, resource, output, or runtime error |
128
+ | `3` | Invalid CLI configuration or usage |
129
+
130
+ ## Python API
131
+
132
+ ```python
133
+ from jsonl_diff import ChangeOperation, diff
134
+
135
+ with diff(
136
+ "old.jsonl.gz",
137
+ "new.jsonl.gz",
138
+ key=("country", "customerId"),
139
+ ignore=("/updated_at",),
140
+ where='country == `"ES"`',
141
+ ) as result:
142
+ print(result.summary)
143
+
144
+ for change in result.changes(ChangeOperation.MODIFIED):
145
+ print(change.key, change.old_line, change.new_line)
146
+ ```
147
+
148
+ `diff()` returns a disk-backed `DiffResult`, used as a context manager.
149
+ Results are streamed lazily through `changes()` rather than materialized in
150
+ memory.
151
+
152
+ The main result models are:
153
+
154
+ ```python
155
+ Summary(equal=10, added=2, deleted=1, modified=3)
156
+ ```
157
+
158
+ and immutable `Change` objects containing the operation, typed identity, and
159
+ OLD/NEW source locations.
160
+
161
+ The public exception hierarchy is rooted at `JsonlDiffError`:
162
+
163
+ * `ConfigurationError`
164
+ * `InputError`
165
+ * `DuplicateKeyError`
166
+ * `ResourceError`
167
+
168
+ See the [Python API reference](docs/python-api.md) for the full callable
169
+ signature, result models, `Decimal` key semantics, and error hierarchy.
170
+
171
+ ## Comparison semantics
172
+
173
+ `jsonl-diff` is intentionally strict:
174
+
175
+ * Every input line must contain exactly one valid top-level JSON object.
176
+ * Blank lines, malformed JSON, duplicate properties, `NaN`, and infinities are rejected.
177
+ * Identity fields must be top-level scalar values (`null` allowed only as one component of a composite key).
178
+ * Identity types remain significant: `"1"` ≠ `1`, `true` ≠ `1`.
179
+ * Duplicate identities fail by default; `--duplicates first`/`last` tolerate them.
180
+ * Object property order is ignored; array order is significant.
181
+ * Numbers are compared by mathematical value: `1`, `1.0`, and `1e0` are equal.
182
+ * Unicode strings are compared without normalization.
183
+ * Changes are reported in deterministic identity order.
184
+
185
+ `--where` selects which records participate; `--key` defines identity;
186
+ `--ignore` removes fields from content comparison. See
187
+ [Comparison semantics](docs/comparison-semantics.md) for the full rules,
188
+ including duplicate handling, ignore-pointer edge cases, `--where` evaluation
189
+ order, and canonical number formatting.
190
+
191
+ ## Sources & compression
192
+
193
+ Supported sources include local paths and HTTP/HTTPS URLs. The Python API
194
+ also accepts file-like objects.
195
+
196
+ Supported compression:
197
+
198
+ * gzip
199
+ * bzip2
200
+ * xz
201
+ * Zstandard on Python 3.14
202
+
203
+ See [architecture](docs/architecture.md) for the full source/compression
204
+ support matrix and how sources are delegated to `py-jsonl`.
205
+
206
+ ## Limitations
207
+
208
+ `jsonl-diff` does **not** provide:
209
+
210
+ * line/position-based diffs
211
+ * field-level diffs or JSON Patch
212
+ * move/rename detection
213
+ * nested identities or automatic key detection
214
+ * fuzzy matching or numeric tolerances
215
+ * unordered-array comparison
216
+ * schema validation or input repair
217
+ * ZIP, database, or cloud-provider inputs
218
+ * GUI or HTML reports
219
+
220
+ It is a **dataset reconciliation tool**, not a general-purpose visual JSON diff.
221
+
222
+ ## Further reading
223
+
224
+ * [Comparison semantics](docs/comparison-semantics.md) — identity, duplicates, ignores, `--where`, canonical numbers, determinism.
225
+ * [Disk-backed architecture](docs/architecture.md) — SQLite index, `max_temp`, cleanup, sources and compression.
226
+ * [Details JSONL format](docs/details-format.md) — machine-readable `--details` output schema.
227
+ * [Python API reference](docs/python-api.md) — full signature, result models, and error hierarchy.
228
+
229
+ ## Development
230
+
231
+ ```bash
232
+ uv sync --group test --group lint
233
+
234
+ uv run pytest
235
+ uv run ruff check --quiet --output-format=concise .
236
+ ```
237
+
238
+ The test suite covers CLI behavior, identity/canonicalization, validation, details output, sources, compression, temporary limits, and cleanup.
239
+
240
+ ## License
241
+
242
+ See [LICENSE](LICENSE).