jsonl-diff 0.1.4__tar.gz → 0.1.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonl-diff
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A lightweight, pure-Python tool for comparing large JSONL/NDJSON datasets without loading them entirely into memory.
5
5
  Author-email: rmoralespp <rmoralespp@gmail.com>
6
6
  License: MIT License
@@ -140,12 +140,11 @@ Record order does not matter.
140
140
  * Exact **[RFC 6901](https://www.rfc-editor.org/info/rfc6901/)** JSON Pointer ignores.
141
141
  * Optional disk-backed observed-schema diff for fields, types, nullability, and requiredness.
142
142
  * Semantic number comparison using `Decimal`.
143
- * Optional `msgspec` accelerator for ~2x faster large diffs (`pip install "jsonl-diff[speedups]"`).
144
143
  * Deterministic summaries and change iteration.
145
144
  * Original OLD/NEW physical line numbers.
146
145
  * Machine-readable JSONL change log with `--details` ([format](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/details-format.md)).
147
146
  * Local, HTTP/HTTPS, file-like, and supported compressed sources.
148
- * Incremental top-level JSON array parsing with `--format json`.
147
+ * Incremental parsing for JSONL/NDJSON by default, with support for top-level JSON arrays via `--format json`.
149
148
  * CLI and Python API using the same comparison engine.
150
149
 
151
150
  ## CLI
@@ -165,14 +164,14 @@ jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
165
164
  | `--key KEY` | Required top-level identity field; repeat or comma-separate for composite keys |
166
165
  | `--ignore POINTER` | RFC 6901 pointer to exclude from content comparison |
167
166
  | `--where EXPRESSION` | JMESPath filter applied to each record |
168
- | `--duplicates POLICY` | `error` (default), or select and report duplicates with `first`/`last` |
169
- | `--missing-key POLICY`| `error` (default), or `null` to treat an absent identity field as `null` |
167
+ | `--duplicates POLICY` | `error` (default), or select and report duplicates with `first`/`last` |
168
+ | `--missing-key POLICY`| `error` (default), or `null` to treat an absent identity field as `null` |
170
169
  | `--details FILE` | Write deterministic machine-readable JSONL changes |
171
- | `--schema-diff` | Compare observed fields, types, nullability, and requiredness |
172
- | `--schema-ignore` | RFC 6901 pointer to exclude from observed-schema profiling |
170
+ | `--schema-diff` | Compare observed fields, types, nullability, and requiredness |
171
+ | `--schema-ignore` | RFC 6901 pointer to exclude from observed-schema profiling |
173
172
  | `--quiet` | Suppress the normal summary |
174
173
  | `--max-temp BYTES` | Best-effort budget for `jsonl-diff` workspace temporary storage |
175
- | `--format FORMAT` | Input format: `jsonl` (default) or a top-level JSON array with `json` |
174
+ | `--format FORMAT` | Input format: `jsonl` (default) or a top-level JSON array with `json` |
176
175
 
177
176
  Examples:
178
177
 
@@ -273,7 +272,9 @@ signature, result models, `Decimal` key semantics, and error hierarchy.
273
272
  elements are the records.
274
273
  * Blank lines, malformed JSON, `NaN`, and infinities are rejected.
275
274
  * Duplicate object property names within a record follow JSON's last-wins semantics: the last occurrence is kept.
276
- * Identity fields must be top-level scalar values (`null` allowed only as one component of a composite key).
275
+ * Identity fields must be top-level JSON values. Object member order is ignored
276
+ and array order is significant (`null` allowed only as one component of a
277
+ composite key).
277
278
  * Identity types remain significant: `"1"` ≠ `1`, `true` ≠ `1`.
278
279
  * Duplicate identities fail by default; `--duplicates first`/`last` select one
279
280
  occurrence, report every discarded occurrence, and return exit code `1`.
@@ -293,9 +294,7 @@ order, and canonical number formatting.
293
294
  ## Sources & compression
294
295
 
295
296
  Supported sources include local paths and HTTP/HTTPS URLs. The Python API
296
- also accepts file-like objects. JSON arrays use `py-jsonl.open_stream()` for
297
- the source and `ijson` for incremental parsing, so compression and remote
298
- source handling are shared with JSONL input.
297
+ also accepts file-like objects.
299
298
 
300
299
  Supported compression:
301
300
 
@@ -305,7 +304,7 @@ Supported compression:
305
304
  * Zstandard on Python 3.14
306
305
 
307
306
  See [architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md) for the full source/compression
308
- support matrix and how sources are delegated to `py-jsonl`.
307
+ support matrix and how sources are managed.
309
308
 
310
309
  ## Limitations
311
310
 
@@ -82,12 +82,11 @@ Record order does not matter.
82
82
  * Exact **[RFC 6901](https://www.rfc-editor.org/info/rfc6901/)** JSON Pointer ignores.
83
83
  * Optional disk-backed observed-schema diff for fields, types, nullability, and requiredness.
84
84
  * Semantic number comparison using `Decimal`.
85
- * Optional `msgspec` accelerator for ~2x faster large diffs (`pip install "jsonl-diff[speedups]"`).
86
85
  * Deterministic summaries and change iteration.
87
86
  * Original OLD/NEW physical line numbers.
88
87
  * Machine-readable JSONL change log with `--details` ([format](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/details-format.md)).
89
88
  * Local, HTTP/HTTPS, file-like, and supported compressed sources.
90
- * Incremental top-level JSON array parsing with `--format json`.
89
+ * Incremental parsing for JSONL/NDJSON by default, with support for top-level JSON arrays via `--format json`.
91
90
  * CLI and Python API using the same comparison engine.
92
91
 
93
92
  ## CLI
@@ -107,14 +106,14 @@ jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
107
106
  | `--key KEY` | Required top-level identity field; repeat or comma-separate for composite keys |
108
107
  | `--ignore POINTER` | RFC 6901 pointer to exclude from content comparison |
109
108
  | `--where EXPRESSION` | JMESPath filter applied to each record |
110
- | `--duplicates POLICY` | `error` (default), or select and report duplicates with `first`/`last` |
111
- | `--missing-key POLICY`| `error` (default), or `null` to treat an absent identity field as `null` |
109
+ | `--duplicates POLICY` | `error` (default), or select and report duplicates with `first`/`last` |
110
+ | `--missing-key POLICY`| `error` (default), or `null` to treat an absent identity field as `null` |
112
111
  | `--details FILE` | Write deterministic machine-readable JSONL changes |
113
- | `--schema-diff` | Compare observed fields, types, nullability, and requiredness |
114
- | `--schema-ignore` | RFC 6901 pointer to exclude from observed-schema profiling |
112
+ | `--schema-diff` | Compare observed fields, types, nullability, and requiredness |
113
+ | `--schema-ignore` | RFC 6901 pointer to exclude from observed-schema profiling |
115
114
  | `--quiet` | Suppress the normal summary |
116
115
  | `--max-temp BYTES` | Best-effort budget for `jsonl-diff` workspace temporary storage |
117
- | `--format FORMAT` | Input format: `jsonl` (default) or a top-level JSON array with `json` |
116
+ | `--format FORMAT` | Input format: `jsonl` (default) or a top-level JSON array with `json` |
118
117
 
119
118
  Examples:
120
119
 
@@ -215,7 +214,9 @@ signature, result models, `Decimal` key semantics, and error hierarchy.
215
214
  elements are the records.
216
215
  * Blank lines, malformed JSON, `NaN`, and infinities are rejected.
217
216
  * Duplicate object property names within a record follow JSON's last-wins semantics: the last occurrence is kept.
218
- * Identity fields must be top-level scalar values (`null` allowed only as one component of a composite key).
217
+ * Identity fields must be top-level JSON values. Object member order is ignored
218
+ and array order is significant (`null` allowed only as one component of a
219
+ composite key).
219
220
  * Identity types remain significant: `"1"` ≠ `1`, `true` ≠ `1`.
220
221
  * Duplicate identities fail by default; `--duplicates first`/`last` select one
221
222
  occurrence, report every discarded occurrence, and return exit code `1`.
@@ -235,9 +236,7 @@ order, and canonical number formatting.
235
236
  ## Sources & compression
236
237
 
237
238
  Supported sources include local paths and HTTP/HTTPS URLs. The Python API
238
- also accepts file-like objects. JSON arrays use `py-jsonl.open_stream()` for
239
- the source and `ijson` for incremental parsing, so compression and remote
240
- source handling are shared with JSONL input.
239
+ also accepts file-like objects.
241
240
 
242
241
  Supported compression:
243
242
 
@@ -247,7 +246,7 @@ Supported compression:
247
246
  * Zstandard on Python 3.14
248
247
 
249
248
  See [architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md) for the full source/compression
250
- support matrix and how sources are delegated to `py-jsonl`.
249
+ support matrix and how sources are managed.
251
250
 
252
251
  ## Limitations
253
252
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonl-diff
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A lightweight, pure-Python tool for comparing large JSONL/NDJSON datasets without loading them entirely into memory.
5
5
  Author-email: rmoralespp <rmoralespp@gmail.com>
6
6
  License: MIT License
@@ -140,12 +140,11 @@ Record order does not matter.
140
140
  * Exact **[RFC 6901](https://www.rfc-editor.org/info/rfc6901/)** JSON Pointer ignores.
141
141
  * Optional disk-backed observed-schema diff for fields, types, nullability, and requiredness.
142
142
  * Semantic number comparison using `Decimal`.
143
- * Optional `msgspec` accelerator for ~2x faster large diffs (`pip install "jsonl-diff[speedups]"`).
144
143
  * Deterministic summaries and change iteration.
145
144
  * Original OLD/NEW physical line numbers.
146
145
  * Machine-readable JSONL change log with `--details` ([format](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/details-format.md)).
147
146
  * Local, HTTP/HTTPS, file-like, and supported compressed sources.
148
- * Incremental top-level JSON array parsing with `--format json`.
147
+ * Incremental parsing for JSONL/NDJSON by default, with support for top-level JSON arrays via `--format json`.
149
148
  * CLI and Python API using the same comparison engine.
150
149
 
151
150
  ## CLI
@@ -165,14 +164,14 @@ jsonl-diff [-h] --key KEY [--ignore IGNORE] [--where EXPRESSION]
165
164
  | `--key KEY` | Required top-level identity field; repeat or comma-separate for composite keys |
166
165
  | `--ignore POINTER` | RFC 6901 pointer to exclude from content comparison |
167
166
  | `--where EXPRESSION` | JMESPath filter applied to each record |
168
- | `--duplicates POLICY` | `error` (default), or select and report duplicates with `first`/`last` |
169
- | `--missing-key POLICY`| `error` (default), or `null` to treat an absent identity field as `null` |
167
+ | `--duplicates POLICY` | `error` (default), or select and report duplicates with `first`/`last` |
168
+ | `--missing-key POLICY`| `error` (default), or `null` to treat an absent identity field as `null` |
170
169
  | `--details FILE` | Write deterministic machine-readable JSONL changes |
171
- | `--schema-diff` | Compare observed fields, types, nullability, and requiredness |
172
- | `--schema-ignore` | RFC 6901 pointer to exclude from observed-schema profiling |
170
+ | `--schema-diff` | Compare observed fields, types, nullability, and requiredness |
171
+ | `--schema-ignore` | RFC 6901 pointer to exclude from observed-schema profiling |
173
172
  | `--quiet` | Suppress the normal summary |
174
173
  | `--max-temp BYTES` | Best-effort budget for `jsonl-diff` workspace temporary storage |
175
- | `--format FORMAT` | Input format: `jsonl` (default) or a top-level JSON array with `json` |
174
+ | `--format FORMAT` | Input format: `jsonl` (default) or a top-level JSON array with `json` |
176
175
 
177
176
  Examples:
178
177
 
@@ -273,7 +272,9 @@ signature, result models, `Decimal` key semantics, and error hierarchy.
273
272
  elements are the records.
274
273
  * Blank lines, malformed JSON, `NaN`, and infinities are rejected.
275
274
  * Duplicate object property names within a record follow JSON's last-wins semantics: the last occurrence is kept.
276
- * Identity fields must be top-level scalar values (`null` allowed only as one component of a composite key).
275
+ * Identity fields must be top-level JSON values. Object member order is ignored
276
+ and array order is significant (`null` allowed only as one component of a
277
+ composite key).
277
278
  * Identity types remain significant: `"1"` ≠ `1`, `true` ≠ `1`.
278
279
  * Duplicate identities fail by default; `--duplicates first`/`last` select one
279
280
  occurrence, report every discarded occurrence, and return exit code `1`.
@@ -293,9 +294,7 @@ order, and canonical number formatting.
293
294
  ## Sources & compression
294
295
 
295
296
  Supported sources include local paths and HTTP/HTTPS URLs. The Python API
296
- also accepts file-like objects. JSON arrays use `py-jsonl.open_stream()` for
297
- the source and `ijson` for incremental parsing, so compression and remote
298
- source handling are shared with JSONL input.
297
+ also accepts file-like objects.
299
298
 
300
299
  Supported compression:
301
300
 
@@ -305,7 +304,7 @@ Supported compression:
305
304
  * Zstandard on Python 3.14
306
305
 
307
306
  See [architecture](https://github.com/rmoralespp/jsonl-diff/blob/main/docs/architecture.md) for the full source/compression
308
- support matrix and how sources are delegated to `py-jsonl`.
307
+ support matrix and how sources are managed.
309
308
 
310
309
  ## Limitations
311
310
 
@@ -26,9 +26,9 @@ try: # Optional C-accelerated content canonicalization (Python 3.10+).
26
26
  except ImportError:
27
27
  _msgspec = None
28
28
 
29
- JsonScalar = Union[str, Decimal, bool, None]
29
+ JsonIdentityValue = Union[str, Decimal, bool, int, None, list, dict]
30
30
  Number = Union[Decimal, int, float]
31
- IdentityKey = Tuple[JsonScalar, ...]
31
+ IdentityKey = Tuple[JsonIdentityValue, ...]
32
32
 
33
33
 
34
34
  class _Num(Decimal):
@@ -605,8 +605,8 @@ def _identity(
605
605
  # record into one indistinguishable identity.
606
606
  if not composite:
607
607
  raise ValueError("identity field {!r} must be a non-null scalar".format(name))
608
- elif not isinstance(value, (str, Decimal, bool, int)):
609
- raise ValueError("identity field {!r} must be a non-null scalar".format(name))
608
+ elif not isinstance(value, (str, Decimal, bool, int, list, dict)):
609
+ raise ValueError("identity field {!r} must be a JSON value".format(name))
610
610
  values.append(value)
611
611
  return tuple(values)
612
612
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jsonl-diff"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "A lightweight, pure-Python tool for comparing large JSONL/NDJSON datasets without loading them entirely into memory."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -597,6 +597,26 @@ class TestDetailsOutput:
597
597
  # Assert
598
598
  assert records[1]["key"] == [12345678901234567890]
599
599
 
600
+ def test_structured_key_is_written_to_details_as_json(
601
+ self,
602
+ write_jsonl,
603
+ tmp_path,
604
+ ):
605
+ old = write_jsonl(
606
+ "old.jsonl",
607
+ [{"id": {"region": "ES", "customer": 7}, "value": "before"}],
608
+ )
609
+ new = write_jsonl(
610
+ "new.jsonl",
611
+ [{"id": {"customer": 7, "region": "ES"}, "value": "after"}],
612
+ )
613
+ details = tmp_path / "changes.jsonl"
614
+
615
+ main([str(old), str(new), "--key", "id", "--details", str(details), "--quiet"])
616
+ records = [json.loads(line) for line in details.read_text(encoding="utf-8").splitlines()]
617
+
618
+ assert records[1]["key"] == [{"customer": 7, "region": "ES"}]
619
+
600
620
  def test_duplicate_events_follow_meta_and_include_policy_outcome(
601
621
  self,
602
622
  write_jsonl,
@@ -131,6 +131,50 @@ class TestDiffSummary:
131
131
 
132
132
 
133
133
  class TestIdentityKeys:
134
+ @pytest.mark.parametrize(
135
+ "identity",
136
+ [
137
+ {"region": "ES", "customer": 7},
138
+ ["ES", 7, {"active": True}],
139
+ ],
140
+ )
141
+ def test_structured_identity_values_are_supported(self, write_jsonl, identity):
142
+ old = write_jsonl("old.jsonl", [{"id": identity, "value": "before"}])
143
+ new = write_jsonl("new.jsonl", [{"id": identity, "value": "after"}])
144
+
145
+ with diff(old, new, key="id") as result:
146
+ change = next(result.changes())
147
+
148
+ assert change.key == (identity,)
149
+ assert change.operation == ChangeOperation.MODIFIED
150
+
151
+ def test_identity_object_member_order_is_ignored(self, write_jsonl):
152
+ old = write_jsonl("old.jsonl", ['{"id":{"a":1,"b":{"x":2,"y":3}}}'])
153
+ new = write_jsonl("new.jsonl", ['{"id":{"b":{"y":3,"x":2},"a":1}}'])
154
+
155
+ with diff(old, new, key="id") as result:
156
+ summary = result.summary
157
+
158
+ assert summary == Summary(equal=1, added=0, deleted=0, modified=0)
159
+
160
+ def test_identity_array_order_is_significant(self, write_jsonl):
161
+ old = write_jsonl("old.jsonl", ['{"id":[1,2]}'])
162
+ new = write_jsonl("new.jsonl", ['{"id":[2,1]}'])
163
+
164
+ with diff(old, new, key="id") as result:
165
+ summary = result.summary
166
+
167
+ assert summary == Summary(equal=0, added=1, deleted=1, modified=0)
168
+
169
+ def test_identity_nested_numbers_use_json_numeric_semantics(self, write_jsonl):
170
+ old = write_jsonl("old.jsonl", ['{"id":{"version":1,"values":[2.0]}}'])
171
+ new = write_jsonl("new.jsonl", ['{"id":{"version":1.0,"values":[2e0]}}'])
172
+
173
+ with diff(old, new, key="id") as result:
174
+ summary = result.summary
175
+
176
+ assert summary == Summary(equal=1, added=0, deleted=0, modified=0)
177
+
134
178
  def test_composite_key_is_returned_as_typed_tuple(self, write_jsonl):
135
179
  # Arrange
136
180
  old = write_jsonl("old.jsonl", [{"country": "ES", "customer": 7, "value": "before"}])
@@ -581,8 +625,6 @@ class TestInputValidation:
581
625
  [
582
626
  ({}, "missing identity field"),
583
627
  ({"id": None}, "must be a non-null scalar"),
584
- ({"id": []}, "must be a non-null scalar"),
585
- ({"id": {}}, "must be a non-null scalar"),
586
628
  ],
587
629
  )
588
630
  def test_invalid_identity_component_is_rejected(self, write_jsonl, record, message):
File without changes
File without changes