Initial commit
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# jay_diff — JSON diff / patch library
|
||||
|
||||
`utils/jay_diff.py` is a lightweight, pure-Python library for computing and
|
||||
applying structural diffs between two JSON-compatible dicts. It is used by
|
||||
`collect.py` and `gui_client.py` to compress the push-server stream: instead
|
||||
of retransmitting full snapshots on every poll, the server sends only the
|
||||
fields that changed since the previous snapshot.
|
||||
|
||||
---
|
||||
|
||||
## Concepts
|
||||
|
||||
A **diff** is itself a plain dict. It describes what changed from an **old**
|
||||
state to a **new** state using one or more of these keys:
|
||||
|
||||
| Key | Meaning |
|
||||
|-----|---------|
|
||||
| `update_add` | Fields whose value changed *or* that are new in `new` |
|
||||
| `update` | Fields whose value changed (must already exist in `old`) |
|
||||
| `add` | Fields that are new in `new` (did not exist in `old`) |
|
||||
| `delete` | Fields that were removed or whose sub-keys were pruned |
|
||||
|
||||
`update_add` combines `update` and `add` into a single key (the mode used
|
||||
everywhere in this project). The separate `update` / `add` form is
|
||||
available via `combine_upd_add=False` for completeness.
|
||||
|
||||
An **empty dict `{}`** returned by `jay_diff_full` means the two states are
|
||||
identical.
|
||||
|
||||
### Delete-delta encoding
|
||||
|
||||
Values inside the `delete` sub-dict carry a specific meaning:
|
||||
|
||||
| Value in delete delta | Effect when merged |
|
||||
|-----------------------|--------------------|
|
||||
| `None` | The **entire key** is deleted from `old` |
|
||||
| A **non-empty dict** | Recurse into the sub-dict and delete those specific sub-keys; the parent key is kept (possibly as `{}`) |
|
||||
| A **scalar** (string, int, …) | The key is deleted from `old` |
|
||||
| `{}` (empty dict) | The key is deleted from `old` |
|
||||
|
||||
This distinction matters when the new state has an empty dict `{}` at a
|
||||
location where the old state had a populated dict. In that case the delete
|
||||
delta carries the populated sub-keys as a non-empty dict — after merging,
|
||||
the parent key is preserved as `{}`. If the key is entirely absent from
|
||||
the new state, `None` is stored in the delta, and the merge removes the key
|
||||
completely.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### Diff functions
|
||||
|
||||
```python
|
||||
jay_diff_full(old, new, combine_upd_add=True) -> dict
|
||||
```
|
||||
The main entry point. Computes a complete diff from `old` to `new`.
|
||||
|
||||
- `combine_upd_add=True` (default) — produces `update_add` + `delete`
|
||||
- `combine_upd_add=False` — produces `update` + `add` + `delete`
|
||||
|
||||
Returns `{}` when `old == new`.
|
||||
|
||||
```python
|
||||
jay_diff_add(old, new) -> dict
|
||||
```
|
||||
Returns keys present in `new` but absent from `old` (additions only).
|
||||
|
||||
```python
|
||||
jay_diff_del(old, new) -> dict
|
||||
```
|
||||
Returns keys present in `old` but absent from `new`, or sub-keys that were
|
||||
removed from a shared dict. Uses `None` as sentinel for entirely-absent
|
||||
dict keys.
|
||||
|
||||
```python
|
||||
jay_diff_upd(old, new) -> dict
|
||||
```
|
||||
Returns keys present in both `old` and `new` whose values differ (updates
|
||||
only; new keys are ignored).
|
||||
|
||||
```python
|
||||
jay_diff_upd_add(old, new) -> dict
|
||||
```
|
||||
Like `jay_diff_upd` but also includes keys new in `new` — equivalent to
|
||||
`jay_diff_full(..., combine_upd_add=True)` minus the delete section.
|
||||
|
||||
---
|
||||
|
||||
### Merge functions
|
||||
|
||||
```python
|
||||
jay_merge_full(old, diff) -> dict
|
||||
```
|
||||
The main entry point. Applies a diff produced by `jay_diff_full` to `old`
|
||||
and returns the reconstructed new state.
|
||||
|
||||
**Bootstrap passthrough:** if `old` is `{}` and `diff` contains none of the
|
||||
recognised diff keys, `diff` is returned as-is. This is how the first
|
||||
message from the push server is handled: the server sends
|
||||
`jay_diff_full({}, record[0])`, which looks like a plain snapshot, and the
|
||||
client stores it directly.
|
||||
|
||||
Applies operations in order: `update` → `update_add` → `add` → `delete`.
|
||||
|
||||
```python
|
||||
jay_merge_update(old, new) -> dict # apply an update/update_add delta
|
||||
jay_merge_add(old, new) -> dict # apply an add delta
|
||||
jay_merge_delete(old, new) -> dict # apply a delete delta
|
||||
```
|
||||
Low-level helpers; prefer `jay_merge_full` unless you need fine-grained
|
||||
control.
|
||||
|
||||
---
|
||||
|
||||
## Round-trip guarantee
|
||||
|
||||
```
|
||||
jay_merge_full(old, jay_diff_full(old, new)) == new
|
||||
```
|
||||
|
||||
This holds for all inputs where values are JSON-compatible scalars (strings,
|
||||
numbers, booleans, `None`, lists) or nested dicts thereof. It is verified
|
||||
by `tests/test_jay_diff_real_data.py` against 674 consecutive pairs of real
|
||||
vehicle snapshots.
|
||||
|
||||
---
|
||||
|
||||
## Wire format
|
||||
|
||||
`jay_diff_full` / `jay_merge_full` are used by the push server when
|
||||
`--diff` is enabled. The diff dict is serialised as a newline-delimited
|
||||
JSON line.
|
||||
|
||||
- The **first** message to a new client is `jay_diff_full({}, record[0])`,
|
||||
which always contains `update_add` with every field of the first record.
|
||||
- Subsequent messages contain only the changed fields.
|
||||
- The format is self-describing: a message with none of the diff keys is
|
||||
treated as a full snapshot by `jay_merge_full` (bootstrap passthrough).
|
||||
|
||||
`null` in JSON round-trips to Python `None`, so the delete sentinel survives
|
||||
serialisation transparently.
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
```
|
||||
tests/test_jay_diff.py — 104 unit tests (all diff/merge functions,
|
||||
corner cases: type changes, empty dicts,
|
||||
deep nesting, round-trips)
|
||||
tests/test_jay_diff_real_data.py — parametrised over 675 real VW snapshots
|
||||
in tests/data/ (674 consecutive pairs):
|
||||
round-trip, identical diff = {}, and
|
||||
empty-diff-is-noop checks
|
||||
```
|
||||
|
||||
Run with:
|
||||
|
||||
```bash
|
||||
pytest tests/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bug history
|
||||
|
||||
### `jay_merge_delete`: crash on aliased dicts (fixed)
|
||||
|
||||
`jay_diff_rev` (used internally by the old `jay_diff_del`) stored references
|
||||
to sub-dicts of `_old` directly in the delete delta without copying them.
|
||||
When `jay_merge_full(old, jay_diff_full(old, new))` was called, some
|
||||
sub-dicts in the delete delta were the same Python objects as sub-dicts in
|
||||
`old`. Iterating `_new.items()` while deleting from `_old` (the same
|
||||
object) raised `RuntimeError: dictionary changed size during iteration`.
|
||||
|
||||
**Fix:** `list(_new.items())` snapshots the items before the loop. Also,
|
||||
`jay_diff_del` was rewritten as a standalone recursive function that never
|
||||
stores aliases to its inputs.
|
||||
|
||||
### `jay_merge_delete`: empty dict wrongly pruned (fixed)
|
||||
|
||||
When the new state had an empty dict `{}` at a location where the old state
|
||||
had a non-empty dict, `jay_merge_delete` would delete all sub-keys and then
|
||||
prune the now-empty parent key entirely. The result differed from `new`,
|
||||
which retained the parent key as `{}`.
|
||||
|
||||
Root cause: the old delta format stored the full old sub-dict for both "key
|
||||
absent from new" and "key present but empty in new" — the two cases were
|
||||
indistinguishable at merge time.
|
||||
|
||||
**Fix:** `jay_diff_del` now stores `None` for dict keys entirely absent from
|
||||
`new`, and a non-empty sub-dict for partial deletions. `jay_merge_delete`
|
||||
no longer prunes: a `None` (or scalar) value in the delta is the explicit
|
||||
signal to remove the parent key.
|
||||
@@ -0,0 +1,4 @@
|
||||
from jay_diff.jay_diff import (
|
||||
jay_diff_add, jay_diff_del, jay_diff_upd, jay_diff_upd_add,
|
||||
jay_diff_full, jay_merge_add, jay_merge_delete, jay_merge_update, jay_merge_full,
|
||||
)
|
||||
@@ -0,0 +1,590 @@
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
def jay_diff_rev(_old, _new):
|
||||
delta = {}
|
||||
if not isinstance(_old, dict):
|
||||
return delta
|
||||
for k in _new.keys():
|
||||
if k in _old:
|
||||
if isinstance(_new[k], dict):
|
||||
res = jay_diff_rev(_old[k], _new[k])
|
||||
if len(res.keys()):
|
||||
delta[k] = res
|
||||
else:
|
||||
_old[k] = _new[k]
|
||||
delta[k] = _new[k]
|
||||
|
||||
return delta
|
||||
|
||||
|
||||
def jay_diff_add(_old, _new):
|
||||
__old = deepcopy(_old)
|
||||
delta = jay_diff_rev(__old, _new)
|
||||
return delta
|
||||
|
||||
|
||||
def jay_diff_del(_old, _new):
|
||||
delta = {}
|
||||
for k in _old:
|
||||
if k in _new:
|
||||
if isinstance(_old[k], dict) and isinstance(_new[k], dict):
|
||||
sub = jay_diff_del(_old[k], _new[k])
|
||||
if sub:
|
||||
delta[k] = sub
|
||||
else:
|
||||
# key absent from _new: None signals "delete entire key" for dicts
|
||||
delta[k] = None if isinstance(_old[k], dict) else _old[k]
|
||||
return delta
|
||||
|
||||
|
||||
def jay_diff_upd(_old, _new, add_if_not_exist=False):
|
||||
delta = {}
|
||||
for k in _new.keys():
|
||||
if k in _old:
|
||||
if isinstance(_new[k], dict):
|
||||
res = jay_diff_upd(_old[k], _new[k], add_if_not_exist)
|
||||
if len(res.keys()):
|
||||
delta[k] = res
|
||||
elif _new[k] != _old[k]:
|
||||
delta[k] = _new[k]
|
||||
elif add_if_not_exist:
|
||||
delta[k] = _new[k]
|
||||
|
||||
return delta
|
||||
|
||||
|
||||
def jay_diff_upd_add(_old, _new):
|
||||
return jay_diff_upd(_old, _new, add_if_not_exist=True)
|
||||
|
||||
|
||||
def jay_diff_full(_old, _new, combine_upd_add=True):
|
||||
diff = {}
|
||||
if combine_upd_add:
|
||||
res = jay_diff_upd_add(_old, _new)
|
||||
if len(res.keys()):
|
||||
diff['update_add'] = res
|
||||
res = jay_diff_del(_old, _new)
|
||||
if len(res.keys()):
|
||||
diff['delete'] = res
|
||||
else:
|
||||
res = jay_diff_upd(_old, _new)
|
||||
if len(res.keys()):
|
||||
diff['update'] = res
|
||||
res = jay_diff_add(_old, _new)
|
||||
if len(res.keys()):
|
||||
diff['add'] = res
|
||||
res = jay_diff_del(_old, _new)
|
||||
if len(res.keys()):
|
||||
diff['delete'] = res
|
||||
|
||||
return diff
|
||||
|
||||
|
||||
def jay_merge_update(_old, _new):
|
||||
for k, v in _new.items():
|
||||
if isinstance(v, dict):
|
||||
r = jay_merge_update(_old.get(k, {}), v)
|
||||
_old[k] = r
|
||||
else:
|
||||
if isinstance(_old, dict):
|
||||
_old[k] = _new[k]
|
||||
else:
|
||||
_old = _new
|
||||
return _old
|
||||
|
||||
|
||||
def jay_merge_add(_old, _new):
|
||||
for k, v in _new.items():
|
||||
if isinstance(v, dict):
|
||||
r = jay_merge_add(_old.get(k, {}), v)
|
||||
if k in _old:
|
||||
_old[k].update(r)
|
||||
else:
|
||||
_old[k] = r
|
||||
else:
|
||||
_old[k] = _new[k]
|
||||
return _old
|
||||
|
||||
|
||||
def jay_merge_delete(_old, _new):
|
||||
for k, v in list(_new.items()):
|
||||
if isinstance(v, dict) and v:
|
||||
if k in _old and isinstance(_old[k], dict):
|
||||
jay_merge_delete(_old[k], v)
|
||||
else:
|
||||
if k in _old:
|
||||
del _old[k]
|
||||
return _old
|
||||
|
||||
|
||||
def jay_merge_full(_old, _diff):
|
||||
_is_diff = False
|
||||
res = _old
|
||||
if "update" in _diff:
|
||||
res = jay_merge_update(res, _diff['update'])
|
||||
_is_diff = True
|
||||
if "update_add" in _diff:
|
||||
res = jay_merge_update(res, _diff['update_add'])
|
||||
_is_diff = True
|
||||
if "add" in _diff:
|
||||
res = jay_merge_add(res, _diff['add'])
|
||||
_is_diff = True
|
||||
if "delete" in _diff:
|
||||
res = jay_merge_delete(res, _diff['delete'])
|
||||
_is_diff = True
|
||||
|
||||
if not _is_diff and not _old:
|
||||
res = _diff
|
||||
|
||||
return res
|
||||
|
||||
def _legacy_assertions() -> None:
|
||||
result = jay_diff_add({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
result = jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'})
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'})
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'd': 'icecream', 'c': 'coke'})
|
||||
assert result == {'d':'icecream'}
|
||||
|
||||
result = jay_diff_add({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'})
|
||||
assert result == {'b':'fries'}
|
||||
|
||||
result = jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}, {'c': {'f': 'coffee'}})
|
||||
assert result == {'c': {'f': 'coffee'}}
|
||||
|
||||
result = jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}})
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_del({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'})
|
||||
assert result == {'d':'icecream'}
|
||||
|
||||
result = jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'})
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'})
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'b': 'fries'})
|
||||
assert result == {'c':'pepsi'}
|
||||
|
||||
result = jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'})
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a': 'hamburger', 'b': 'fries'})
|
||||
assert result == {'c': None}
|
||||
|
||||
result = jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'coke'}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'f': 'coffee'}}
|
||||
|
||||
result = jay_diff_del({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}})
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_upd({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'potatoes'})
|
||||
assert result == {'a':'hotdog', 'b':'potatoes'}
|
||||
|
||||
result = jay_diff_upd({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'pepsi'}})
|
||||
assert result == {'c': {'e': 'pepsi'}}
|
||||
|
||||
result = jay_diff_upd({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'fries'})
|
||||
assert result == {'a':'hotdog'}
|
||||
|
||||
result = jay_diff_upd({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'd': 'icecream', 'c': 'coke'})
|
||||
assert result == {'c':'coke'}
|
||||
|
||||
result = jay_diff_upd({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'})
|
||||
assert result == {'c':'coke'}
|
||||
|
||||
result = jay_diff_upd({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}, {'c': {'f': 'coffee'}})
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_upd({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}})
|
||||
assert result == {'c': {'e': 'pepsi'}}
|
||||
|
||||
# Update/add
|
||||
result = jay_diff_upd_add({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'potatoes', 'd': 'icecream'})
|
||||
assert result == {'a':'hotdog', 'b':'potatoes', 'd': 'icecream'}
|
||||
|
||||
result = jay_diff_upd_add({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}, 'b':'potatoes'})
|
||||
assert result == {'b':'potatoes', 'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}}
|
||||
|
||||
result = jay_diff_upd_add({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'fries'})
|
||||
assert result == {'a':'hotdog'}
|
||||
|
||||
result = jay_diff_upd_add({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}})
|
||||
assert result == {'c': {'e': 'pepsi'}}
|
||||
|
||||
#####################
|
||||
result = jay_diff_upd_add({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'potatoes'})
|
||||
assert result == {'a':'hotdog', 'b':'potatoes'}
|
||||
|
||||
result = jay_diff_upd_add({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'pepsi'}})
|
||||
assert result == {'c': {'e': 'pepsi'}}
|
||||
|
||||
result = jay_diff_upd_add({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'fries'})
|
||||
assert result == {'a':'hotdog'}
|
||||
|
||||
result = jay_diff_upd_add({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'd': 'icecream', 'c': 'coke'})
|
||||
assert result == {'c': 'coke', 'd': 'icecream'}
|
||||
|
||||
result = jay_diff_upd_add({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'})
|
||||
assert result == {'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_diff_upd_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}, {'c': {'f': 'coffee'}})
|
||||
assert result == {'c': {'f': 'coffee'}}
|
||||
|
||||
result = jay_diff_upd_add({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}})
|
||||
assert result == {'c': {'e': 'pepsi'}}
|
||||
|
||||
result = jay_diff_upd_add({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee', 'h': 'cake'}})
|
||||
assert result == {'c': {'e': 'pepsi', 'h': 'cake'}}
|
||||
|
||||
# jay_diff_full
|
||||
combine_upd_add = False
|
||||
|
||||
result = jay_diff_full({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'add': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'update': {'c': 'coke'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, combine_upd_add)
|
||||
assert result == {'delete': {'d': 'icecream'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'add': {'d': 'icecream'}, 'delete': {'b': 'fries'}, 'update': {'c': 'coke'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'add': {'b': 'fries'}, 'delete': {'d': 'icecream'}, 'update': {'c': 'coke'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}, {'c': {'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'add': {'c': {'f': 'coffee'}}, 'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'add': {'b': 'fries'}, 'delete': {'d': 'icecream'}, 'update': {'c': 'coke'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, combine_upd_add)
|
||||
assert result == {'update': {'c': 'pepsi'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'b': 'fries'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': 'pepsi'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'update': {'c': 'coke'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a': 'hamburger', 'b': 'fries'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': None}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'delete': {'c': {'e': 'coke'}}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'coke'}}, combine_upd_add)
|
||||
assert result == {'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'potatoes'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': None}, 'update': {'a': 'hotdog', 'b': 'potatoes'}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'pepsi'}}, combine_upd_add)
|
||||
assert result == {'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}, 'update': {'c': {'e': 'pepsi'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'fries'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': None}, 'update': {'a': 'hotdog'}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'update': {'c': {'e': 'pepsi'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'potatoes', 'd': 'icecream'}, combine_upd_add)
|
||||
assert result == {'add': {'d': 'icecream'}, 'delete': {'c': None}, 'update': {'a': 'hotdog', 'b': 'potatoes'}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}, 'b':'potatoes'}, combine_upd_add)
|
||||
assert result == {'add': {'c': {'g': 'tea', 'h': 'cake'}}, 'delete': {'a': 'hamburger', 'c': {'f': 'coffee'}}, 'update': {'b': 'potatoes', 'c': {'e': 'pepsi'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'fries'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': None}, 'update': {'a': 'hotdog'}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'update': {'c': {'e': 'pepsi'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee', 'h': 'cake'}}, combine_upd_add)
|
||||
assert result == {'add': {'c': {'h': 'cake'}}, 'update': {'c': {'e': 'pepsi'}}}
|
||||
|
||||
# ToDo: jay_diff_full
|
||||
combine_upd_add = True
|
||||
|
||||
result = jay_diff_full({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'update_add': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'update_add': {'c': 'coke'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, combine_upd_add)
|
||||
assert result == {'delete': {'d': 'icecream'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'delete': {'b': 'fries'}, 'update_add': {'c': 'coke', 'd': 'icecream'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'delete': {'d': 'icecream'}, 'update_add': {'b': 'fries', 'c': 'coke'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}, {'c': {'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'update_add': {'c': {'f': 'coffee'}}, 'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'delete': {'d': 'icecream'}, 'update_add': {'b': 'fries', 'c': 'coke'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, combine_upd_add)
|
||||
assert result == {'update_add': {'c': 'pepsi'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'b': 'fries'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': 'pepsi'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, combine_upd_add)
|
||||
assert result == {'update_add': {'c': 'coke'}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a': 'hamburger', 'b': 'fries'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': None}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'delete': {'c': {'e': 'coke'}}}
|
||||
|
||||
result = jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'coke'}}, combine_upd_add)
|
||||
assert result == {'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'potatoes'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': None}, 'update_add': {'a': 'hotdog', 'b': 'potatoes'}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'pepsi'}}, combine_upd_add)
|
||||
assert result == {'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}, 'update_add': {'c': {'e': 'pepsi'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'fries'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': None}, 'update_add': {'a': 'hotdog'}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'update_add': {'c': {'e': 'pepsi'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'potatoes', 'd': 'icecream'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': None}, 'update_add': {'a': 'hotdog', 'b': 'potatoes', 'd': 'icecream'}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}, 'b':'potatoes'}, combine_upd_add)
|
||||
assert result == {'delete': {'a': 'hamburger', 'c': {'f': 'coffee'}}, 'update_add': {'b': 'potatoes', 'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hotdog', 'b':'fries'}, combine_upd_add)
|
||||
assert result == {'delete': {'c': None}, 'update_add': {'a': 'hotdog'}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}, combine_upd_add)
|
||||
assert result == {'update_add': {'c': {'e': 'pepsi'}}}
|
||||
|
||||
result = jay_diff_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee', 'h': 'cake'}}, combine_upd_add)
|
||||
assert result == {'update_add': {'c': {'e': 'pepsi', 'h': 'cake'}}}
|
||||
|
||||
# Merge
|
||||
result = jay_merge_add({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'d': 'icecream'})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}, 'd':'icecream'}
|
||||
|
||||
result = jay_merge_add({'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}, {'d': 'icecream'})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'f': 'coffee'}, 'd':'icecream'}
|
||||
|
||||
result = jay_merge_add({'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}, 'd': 'icecream'}, {'d': 'icecream'})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'f': 'coffee'}, 'd':'icecream'}
|
||||
|
||||
result = jay_merge_delete({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}, 'd': 'icecream'}, {'d': 'icecream'})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_delete({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'f': 'coffee'}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'coke'}}
|
||||
|
||||
result = jay_merge_delete({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'c': {'e': 'coke'}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'f': 'coffee'}}
|
||||
|
||||
# jay_merge_full
|
||||
# the no-jaydiff case
|
||||
result = jay_merge_full({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
# combine_upd_add = False
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'add': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'update': {'c': 'pepsi'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi', 'd': 'icecream'}, {'delete': {'d': 'icecream'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'add': {'d': 'icecream'}, 'delete': {'b': 'fries'}, 'update': {'c': 'coke'}})
|
||||
assert result == {'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'add': {'b': 'fries'}, 'delete': {'d': 'icecream'}, 'update': {'c': 'coke'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}, {'add': {'c': {'f': 'coffee'}}, 'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}})
|
||||
assert result == {'c': {'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'add': {'b': 'fries'}, 'delete': {'d': 'icecream'}, 'update': {'c': 'coke'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'update': {'c': 'pepsi'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'delete': {'c': 'pepsi'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'update': {'c': 'coke'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': None}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': {'e': 'coke'}}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}})
|
||||
assert result == {'c': {'e': 'coke'}}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': None}, 'update': {'a': 'hotdog', 'b': 'potatoes'}})
|
||||
assert result == {'a':'hotdog', 'b':'potatoes'}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}, 'update': {'c': {'e': 'pepsi'}}})
|
||||
assert result == {'c': {'e': 'pepsi'}}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': None}, 'update': {'a': 'hotdog'}})
|
||||
assert result == {'a':'hotdog', 'b':'fries'}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'update': {'c': {'e': 'pepsi'}}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'add': {'d': 'icecream'}, 'delete': {'c': None}, 'update': {'a': 'hotdog', 'b': 'potatoes'}})
|
||||
assert result == {'a':'hotdog', 'b':'potatoes', 'd': 'icecream'}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'add': {'c': {'g': 'tea', 'h': 'cake'}}, 'delete': {'a': 'hamburger', 'c': {'f': 'coffee'}}, 'update': {'b': 'potatoes', 'c': {'e': 'pepsi'}}})
|
||||
assert result == {'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}, 'b':'potatoes'}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': None}, 'update': {'a': 'hotdog'}})
|
||||
assert result == {'a':'hotdog', 'b':'fries'}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'update': {'c': {'e': 'pepsi'}}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'add': {'c': {'h': 'cake'}}, 'update': {'c': {'e': 'pepsi'}}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee', 'h': 'cake'}}
|
||||
|
||||
# ToDo: jay_merge_full
|
||||
# combine_upd_add = True
|
||||
|
||||
result = jay_merge_full({}, {'update_add': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'update_add': {'c': 'coke'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi', 'd': 'icecream'}, {'delete': {'d': 'icecream'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'delete': {'b': 'fries'}, 'update_add': {'c': 'coke', 'd': 'icecream'}})
|
||||
assert result == {'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'delete': {'d': 'icecream'}, 'update_add': {'b': 'fries', 'c': 'coke'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}, {'update_add': {'c': {'f': 'coffee'}}, 'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}})
|
||||
assert result == {'c': {'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'}, {'delete': {'d': 'icecream'}, 'update_add': {'b': 'fries', 'c': 'coke'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'update_add': {'c': 'pepsi'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'delete': {'c': 'pepsi'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'update_add': {'c': 'coke'}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': None}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries'}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': {'e': 'coke'}}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}})
|
||||
assert result == {'c': {'e': 'coke'}}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': None}, 'update_add': {'a': 'hotdog', 'b': 'potatoes'}})
|
||||
assert result == {'a':'hotdog', 'b':'potatoes'}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}, 'update_add': {'c': {'e': 'pepsi'}}})
|
||||
assert result == {'c': {'e': 'pepsi'}}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': None}, 'update_add': {'a': 'hotdog'}})
|
||||
assert result == {'a':'hotdog', 'b':'fries'}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'update_add': {'c': {'e': 'pepsi'}}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': None}, 'update_add': {'a': 'hotdog', 'b': 'potatoes', 'd': 'icecream'}})
|
||||
assert result == {'a':'hotdog', 'b':'potatoes', 'd': 'icecream'}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'a': 'hamburger', 'c': {'f': 'coffee'}}, 'update_add': {'b': 'potatoes', 'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}}})
|
||||
assert result == {'b':'potatoes', 'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'delete': {'c': None}, 'update_add': {'a': 'hotdog'}})
|
||||
assert result == {'a':'hotdog', 'b':'fries'}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'update_add': {'c': {'e': 'pepsi'}}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({'a':'hamburger', 'b':'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, {'update_add': {'c': {'e': 'pepsi', 'h': 'cake'}}})
|
||||
assert result == {'a':'hamburger', 'b':'fries', 'c': {'e': 'pepsi', 'f': 'coffee', 'h': 'cake'}}
|
||||
|
||||
result = jay_merge_full({}, {'add': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
result = jay_merge_full({}, {'add': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': {'coffee': {'h': 'black'}}}}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': {'coffee': {'h': 'black'}}}}
|
||||
|
||||
result = jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': {'coffee': {'h': 'black'}}}}, {'add': {'c': {'f': {'coffee': {'j': 'sugar'}}}}})
|
||||
assert result == {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': {'coffee': {'h': 'black', 'j': 'sugar'}}}}
|
||||
|
||||
old = {'charging': {
|
||||
'batteryStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:34Z', 'cruisingRangeElectric_km': 192}},
|
||||
'chargingStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:34Z'}},
|
||||
'chargingSettings': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:30Z'}},
|
||||
'plugStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:29Z'}}}}
|
||||
update = {'charging': {
|
||||
'batteryStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:35:46Z', 'cruisingRangeElectric_km': 191}},
|
||||
'chargingStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:35:46Z'}}}}
|
||||
add = {'charging': {
|
||||
'batteryStatus': {'value': {'currentSOC_pct': 55}}},
|
||||
'fuelStatus': {'rangeStatus': {'value': {'primaryEngine': {'currentSOC_pct': 55}}}},
|
||||
'measurements': {'fuelLevelStatus': {'value': {'currentSOC_pct': 55}}}}
|
||||
|
||||
delete = {'charging': {
|
||||
'chargingSettings': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:30Z'}},
|
||||
'plugStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:29Z'}}}}
|
||||
|
||||
result = jay_merge_full(old, {'update':update, 'add':add, 'delete':delete})
|
||||
assert result == old
|
||||
|
||||
if __name__ == '__main__':
|
||||
_legacy_assertions()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"meta": {
|
||||
"vin": "WVWZZZE1ZMP010760",
|
||||
"created_at": "2026-05-26T11:37:05.156831+00:00",
|
||||
"interval_seconds": 30
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"ts": "2026-05-26T11:37:08.127749+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "ChargingState.CHARGE_PURPOSE_REACHED_CONSERVATION",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"chargeType": "ChargeType.AC"
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 80,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 280,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": "MaximumChargeCurrent.MAXIMUM",
|
||||
"autoUnlockPlugWhenCharged": "UnlockPlugState.OFF"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "PlugConnectionState.CONNECTED",
|
||||
"plugLockState": "PlugLockState.LOCKED",
|
||||
"externalPower": "ExternalPower.READY"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67419,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 280,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 280,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 23.5,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 22.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "ClimatizationState.OFF",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
},
|
||||
"unitInCar": "UnitInCar.CELSIUS"
|
||||
},
|
||||
"windowHeatingStatus": {
|
||||
"front": "WindowHeatingState.OFF",
|
||||
"rear": "WindowHeatingState.OFF"
|
||||
}
|
||||
},
|
||||
"access": {
|
||||
"accessStatus": {
|
||||
"overallStatus": "OverallState.SAFE",
|
||||
"doors": {
|
||||
"bonnet": {
|
||||
"lockState": "LockState.UNKNOWN",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"trunk": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"rearRight": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"rearLeft": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"frontRight": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"frontLeft": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
}
|
||||
},
|
||||
"windows": {
|
||||
"sunRoof": {
|
||||
"openState": "OpenState.UNSUPPORTED"
|
||||
},
|
||||
"roofCover": {
|
||||
"openState": "OpenState.UNSUPPORTED"
|
||||
},
|
||||
"sunRoofRear": {
|
||||
"openState": "OpenState.UNSUPPORTED"
|
||||
},
|
||||
"frontLeft": {
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"frontRight": {
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"rearLeft": {
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"rearRight": {
|
||||
"openState": "OpenState.CLOSED"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-26T11:37:43.660250+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "ChargingState.CHARGE_PURPOSE_REACHED_CONSERVATION",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"chargeType": "ChargeType.AC"
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 80,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 280,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": "MaximumChargeCurrent.MAXIMUM",
|
||||
"autoUnlockPlugWhenCharged": "UnlockPlugState.OFF"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "PlugConnectionState.CONNECTED",
|
||||
"plugLockState": "PlugLockState.LOCKED",
|
||||
"externalPower": "ExternalPower.READY"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67419,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 280,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 280,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 23.5,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 22.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "ClimatizationState.OFF",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
},
|
||||
"unitInCar": "UnitInCar.CELSIUS"
|
||||
},
|
||||
"windowHeatingStatus": {
|
||||
"front": "WindowHeatingState.OFF",
|
||||
"rear": "WindowHeatingState.OFF"
|
||||
}
|
||||
},
|
||||
"access": {
|
||||
"accessStatus": {
|
||||
"overallStatus": "OverallState.SAFE",
|
||||
"doors": {
|
||||
"bonnet": {
|
||||
"lockState": "LockState.UNKNOWN",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"trunk": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"rearRight": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"rearLeft": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"frontRight": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"frontLeft": {
|
||||
"lockState": "LockState.LOCKED",
|
||||
"openState": "OpenState.CLOSED"
|
||||
}
|
||||
},
|
||||
"windows": {
|
||||
"sunRoof": {
|
||||
"openState": "OpenState.UNSUPPORTED"
|
||||
},
|
||||
"roofCover": {
|
||||
"openState": "OpenState.UNSUPPORTED"
|
||||
},
|
||||
"sunRoofRear": {
|
||||
"openState": "OpenState.UNSUPPORTED"
|
||||
},
|
||||
"frontLeft": {
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"frontRight": {
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"rearLeft": {
|
||||
"openState": "OpenState.CLOSED"
|
||||
},
|
||||
"rearRight": {
|
||||
"openState": "OpenState.CLOSED"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,485 @@
|
||||
{
|
||||
"meta": {
|
||||
"vin": "WVWZZZE1ZMP010760",
|
||||
"created_at": "2026-05-27T05:53:07.953894+00:00",
|
||||
"interval_seconds": 30
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"ts": "2026-05-27T05:53:08.103087+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "off",
|
||||
"chargeType": "invalid",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"autoUnlockPlugWhenCharged": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "disconnected",
|
||||
"plugLockState": "unlocked",
|
||||
"externalPower": "unavailable"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"temperatureOutsideStatus": {
|
||||
"temperatureOutside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"readiness": {
|
||||
"readinessStatus": {
|
||||
"connectionState": {
|
||||
"isOnline": false,
|
||||
"isActive": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "off",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parking": {
|
||||
"parkingPosition": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
}
|
||||
},
|
||||
"access": {
|
||||
"accessStatus": {
|
||||
"overallStatus": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-27T05:53:38.257550+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "off",
|
||||
"chargeType": "invalid",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"autoUnlockPlugWhenCharged": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "disconnected",
|
||||
"plugLockState": "unlocked",
|
||||
"externalPower": "unavailable"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"temperatureOutsideStatus": {
|
||||
"temperatureOutside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"readiness": {
|
||||
"readinessStatus": {
|
||||
"connectionState": {
|
||||
"isOnline": false,
|
||||
"isActive": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "off",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parking": {
|
||||
"parkingPosition": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
}
|
||||
},
|
||||
"access": {
|
||||
"accessStatus": {
|
||||
"overallStatus": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-27T05:54:08.438969+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "off",
|
||||
"chargeType": "invalid",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"autoUnlockPlugWhenCharged": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "disconnected",
|
||||
"plugLockState": "unlocked",
|
||||
"externalPower": "unavailable"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"temperatureOutsideStatus": {
|
||||
"temperatureOutside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"readiness": {
|
||||
"readinessStatus": {
|
||||
"connectionState": {
|
||||
"isOnline": false,
|
||||
"isActive": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "off",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parking": {
|
||||
"parkingPosition": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
}
|
||||
},
|
||||
"access": {
|
||||
"accessStatus": {
|
||||
"overallStatus": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-27T05:54:38.603661+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "off",
|
||||
"chargeType": "invalid",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"autoUnlockPlugWhenCharged": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "disconnected",
|
||||
"plugLockState": "unlocked",
|
||||
"externalPower": "unavailable"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"temperatureOutsideStatus": {
|
||||
"temperatureOutside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"readiness": {
|
||||
"readinessStatus": {
|
||||
"connectionState": {
|
||||
"isOnline": false,
|
||||
"isActive": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "off",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parking": {
|
||||
"parkingPosition": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
}
|
||||
},
|
||||
"access": {
|
||||
"accessStatus": {
|
||||
"overallStatus": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"meta": {
|
||||
"vin": "WVWZZZE1ZMP010760",
|
||||
"created_at": "2026-05-27T05:55:03.027525+00:00",
|
||||
"interval_seconds": 30
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"ts": "2026-05-27T05:55:03.219238+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "off",
|
||||
"chargeType": "invalid",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"autoUnlockPlugWhenCharged": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "disconnected",
|
||||
"plugLockState": "unlocked",
|
||||
"externalPower": "unavailable"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"temperatureOutsideStatus": {
|
||||
"temperatureOutside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"readiness": {
|
||||
"readinessStatus": {
|
||||
"connectionState": {
|
||||
"isOnline": false,
|
||||
"isActive": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "off",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parking": {
|
||||
"parkingPosition": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
}
|
||||
},
|
||||
"access": {
|
||||
"accessStatus": {
|
||||
"overallStatus": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"meta": {
|
||||
"vin": "WVWZZZE1ZMP010760",
|
||||
"created_at": "2026-05-27T06:17:18.385566+00:00",
|
||||
"interval_seconds": 30
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"ts": "2026-05-27T06:17:18.554491+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "off",
|
||||
"chargeType": "invalid",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"autoUnlockPlugWhenCharged": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "disconnected",
|
||||
"plugLockState": "unlocked",
|
||||
"externalPower": "unavailable"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"temperatureOutsideStatus": {
|
||||
"temperatureOutside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
}
|
||||
},
|
||||
"connectivity": {
|
||||
"connection": {
|
||||
"state": "reachable"
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "off",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
}
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"meta": {
|
||||
"vin": "WVWZZZE1ZMP010760",
|
||||
"created_at": "2026-05-27T06:18:28.783719+00:00",
|
||||
"interval_seconds": 30
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"ts": "2026-05-27T06:18:28.981339+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "off",
|
||||
"chargeType": "invalid",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"autoUnlockPlugWhenCharged": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "disconnected",
|
||||
"plugLockState": "unlocked",
|
||||
"externalPower": "unavailable"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"temperatureOutsideStatus": {
|
||||
"temperatureOutside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
}
|
||||
},
|
||||
"connectivity": {
|
||||
"connection": {
|
||||
"state": "reachable"
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "off",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
}
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"meta": {
|
||||
"vin": "WVWZZZE1ZMP010760",
|
||||
"created_at": "2026-05-27T06:21:01.933728+00:00",
|
||||
"interval_seconds": 30
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"ts": "2026-05-27T06:21:02.150738+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "off",
|
||||
"chargeType": "invalid",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"autoUnlockPlugWhenCharged": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "disconnected",
|
||||
"plugLockState": "unlocked",
|
||||
"externalPower": "unavailable"
|
||||
}
|
||||
},
|
||||
"measurements": {
|
||||
"odometerStatus": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"rangeStatus": {
|
||||
"electricRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"totalRange": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"temperatureBatteryStatus": {
|
||||
"temperatureHvBatteryMax": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperatureHvBatteryMin": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"temperatureOutsideStatus": {
|
||||
"temperatureOutside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
}
|
||||
},
|
||||
"connectivity": {
|
||||
"connection": {
|
||||
"state": "reachable"
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "off",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
}
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"meta": {
|
||||
"vin": "WVWZZZE1ZMP010760",
|
||||
"created_at": "2026-05-27T06:33:08.561803+00:00",
|
||||
"interval_seconds": 30
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"ts": "2026-05-27T06:33:08.745827+00:00",
|
||||
"charging": {
|
||||
"chargingStatus": {
|
||||
"chargingState": "off",
|
||||
"chargeType": "invalid",
|
||||
"chargePower": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remainingChargingTimeToComplete": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"batteryStatus": {
|
||||
"currentSOC": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"cruisingRangeElectric": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"chargingSettings": {
|
||||
"targetSOC": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maxChargeCurrentAC": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"autoUnlockPlugWhenCharged": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"plugConnectionState": "disconnected",
|
||||
"plugLockState": "unlocked",
|
||||
"externalPower": "unavailable"
|
||||
}
|
||||
},
|
||||
"electric_system": {
|
||||
"electric_system": {
|
||||
"range": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"range_full": {
|
||||
"value": 360.3448275862069,
|
||||
"unit": "km"
|
||||
},
|
||||
"battery": {
|
||||
"temperature_max": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperature_min": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"odometer": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"temperature_outside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
}
|
||||
},
|
||||
"connectivity": {
|
||||
"connectivity": {
|
||||
"state": "reachable"
|
||||
}
|
||||
},
|
||||
"climatisation": {
|
||||
"climatisationStatus": {
|
||||
"climatisationState": "off",
|
||||
"remainingClimatisationTime": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
}
|
||||
},
|
||||
"climatisationSettings": {
|
||||
"targetTemperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
}
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,627 @@
|
||||
{
|
||||
"meta": {
|
||||
"vin": "WVWZZZE1ZMP010760",
|
||||
"created_at": "2026-05-27T09:31:50.175248+00:00",
|
||||
"interval_seconds": 30
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"ts": "2026-05-27T09:31:50.333801+00:00",
|
||||
"charging": {
|
||||
"state": "off",
|
||||
"type": "invalid",
|
||||
"power": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_level": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maximum_current": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"auto_unlock": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"connection_state": "disconnected",
|
||||
"lock_state": "unlocked",
|
||||
"external_power": "unavailable"
|
||||
}
|
||||
},
|
||||
"electric_drive": {
|
||||
"range": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"range_full": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
},
|
||||
"battery": {
|
||||
"soc": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"temperature_max": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperature_min": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"odometer": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
},
|
||||
"connectivity": {
|
||||
"state": "reachable"
|
||||
},
|
||||
"climatisation": {
|
||||
"state": "off",
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_temperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"temperature_outside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-27T09:32:20.484182+00:00",
|
||||
"charging": {
|
||||
"state": "off",
|
||||
"type": "invalid",
|
||||
"power": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_level": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maximum_current": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"auto_unlock": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"connection_state": "disconnected",
|
||||
"lock_state": "unlocked",
|
||||
"external_power": "unavailable"
|
||||
}
|
||||
},
|
||||
"electric_drive": {
|
||||
"range": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"range_full": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
},
|
||||
"battery": {
|
||||
"soc": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"temperature_max": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperature_min": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"odometer": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
},
|
||||
"connectivity": {
|
||||
"state": "reachable"
|
||||
},
|
||||
"climatisation": {
|
||||
"state": "off",
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_temperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"temperature_outside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-27T09:32:50.642394+00:00",
|
||||
"charging": {
|
||||
"state": "off",
|
||||
"type": "invalid",
|
||||
"power": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_level": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maximum_current": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"auto_unlock": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"connection_state": "disconnected",
|
||||
"lock_state": "unlocked",
|
||||
"external_power": "unavailable"
|
||||
}
|
||||
},
|
||||
"electric_drive": {
|
||||
"range": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"range_full": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
},
|
||||
"battery": {
|
||||
"soc": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"temperature_max": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperature_min": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"odometer": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
},
|
||||
"connectivity": {
|
||||
"state": "reachable"
|
||||
},
|
||||
"climatisation": {
|
||||
"state": "off",
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_temperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"temperature_outside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-27T09:33:20.808989+00:00",
|
||||
"charging": {
|
||||
"state": "off",
|
||||
"type": "invalid",
|
||||
"power": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_level": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maximum_current": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"auto_unlock": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"connection_state": "disconnected",
|
||||
"lock_state": "unlocked",
|
||||
"external_power": "unavailable"
|
||||
}
|
||||
},
|
||||
"electric_drive": {
|
||||
"range": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"range_full": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
},
|
||||
"battery": {
|
||||
"soc": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"temperature_max": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperature_min": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"odometer": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
},
|
||||
"connectivity": {
|
||||
"state": "reachable"
|
||||
},
|
||||
"climatisation": {
|
||||
"state": "off",
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_temperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"temperature_outside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-27T09:33:50.960434+00:00",
|
||||
"charging": {
|
||||
"state": "off",
|
||||
"type": "invalid",
|
||||
"power": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_level": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maximum_current": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"auto_unlock": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"connection_state": "disconnected",
|
||||
"lock_state": "unlocked",
|
||||
"external_power": "unavailable"
|
||||
}
|
||||
},
|
||||
"electric_drive": {
|
||||
"range": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"range_full": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
},
|
||||
"battery": {
|
||||
"soc": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"temperature_max": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperature_min": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"odometer": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
},
|
||||
"connectivity": {
|
||||
"state": "reachable"
|
||||
},
|
||||
"climatisation": {
|
||||
"state": "off",
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_temperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"temperature_outside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-27T09:34:21.107584+00:00",
|
||||
"charging": {
|
||||
"state": "off",
|
||||
"type": "invalid",
|
||||
"power": {
|
||||
"value": 0.0,
|
||||
"unit": "kW"
|
||||
},
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_level": {
|
||||
"value": 80.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"maximum_current": {
|
||||
"value": 16.0,
|
||||
"unit": "A"
|
||||
},
|
||||
"auto_unlock": "False"
|
||||
},
|
||||
"plugStatus": {
|
||||
"connection_state": "disconnected",
|
||||
"lock_state": "unlocked",
|
||||
"external_power": "unavailable"
|
||||
}
|
||||
},
|
||||
"electric_drive": {
|
||||
"range": {
|
||||
"value": 209.0,
|
||||
"unit": "km"
|
||||
},
|
||||
"range_full": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
},
|
||||
"battery": {
|
||||
"soc": {
|
||||
"value": 58.0,
|
||||
"unit": "%"
|
||||
},
|
||||
"temperature_max": {
|
||||
"value": 295.15,
|
||||
"unit": "degC"
|
||||
},
|
||||
"temperature_min": {
|
||||
"value": 294.15,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"odometer": {
|
||||
"odometer": {
|
||||
"value": 67489.0,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"state": "unknown vehicle state"
|
||||
},
|
||||
"connectivity": {
|
||||
"state": "reachable"
|
||||
},
|
||||
"climatisation": {
|
||||
"state": "off",
|
||||
"remain": {
|
||||
"value": 0,
|
||||
"unit": "min"
|
||||
},
|
||||
"settings": {
|
||||
"target_temperature": {
|
||||
"value": 21.0,
|
||||
"unit": "degC"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"temperature_outside": {
|
||||
"value": null,
|
||||
"unit": "degC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"lat": null,
|
||||
"lon": null
|
||||
},
|
||||
"doors": {
|
||||
"doors": {
|
||||
"overallState": "None",
|
||||
"doors": {},
|
||||
"windows": {}
|
||||
}
|
||||
},
|
||||
"procedural": {
|
||||
"range_at_100": {
|
||||
"value": 360.3,
|
||||
"unit": "km"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,532 @@
|
||||
"""Tests for jay_diff."""
|
||||
import pytest
|
||||
from copy import deepcopy
|
||||
|
||||
from jay_diff import (
|
||||
jay_diff_add, jay_diff_del, jay_diff_upd, jay_diff_upd_add,
|
||||
jay_diff_full, jay_merge_add, jay_merge_delete, jay_merge_update, jay_merge_full,
|
||||
)
|
||||
|
||||
|
||||
# ── jay_diff_add ──────────────────────────────────────────────────────────────
|
||||
|
||||
class TestJayDiffAdd:
|
||||
def test_add_to_empty(self):
|
||||
assert jay_diff_add({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
def test_no_new_keys(self):
|
||||
assert jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'coke'}) == {}
|
||||
|
||||
def test_old_has_extra_keys(self):
|
||||
assert jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi', 'd': 'icecream'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}) == {}
|
||||
|
||||
def test_one_new_key(self):
|
||||
assert jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}) == {'d': 'icecream'}
|
||||
|
||||
def test_new_key_among_overlap(self):
|
||||
assert jay_diff_add({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'coke'}) == {'b': 'fries'}
|
||||
|
||||
def test_nested_new_key(self):
|
||||
assert jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}},
|
||||
{'c': {'f': 'coffee'}}) == {'c': {'f': 'coffee'}}
|
||||
|
||||
def test_nested_no_new_keys(self):
|
||||
assert jay_diff_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}) == {}
|
||||
|
||||
|
||||
# ── jay_diff_del ──────────────────────────────────────────────────────────────
|
||||
|
||||
class TestJayDiffDel:
|
||||
def test_one_deleted_key(self):
|
||||
assert jay_diff_del({'a': 'hamburger', 'c': 'pepsi', 'd': 'icecream'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'coke'}) == {'d': 'icecream'}
|
||||
|
||||
def test_no_deleted_keys_same_values(self):
|
||||
assert jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': 'coke'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'coke'}) == {}
|
||||
|
||||
def test_no_deleted_keys_different_values(self):
|
||||
assert jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': 'coke'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}) == {}
|
||||
|
||||
def test_key_absent_from_new(self):
|
||||
assert jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'a': 'hamburger', 'b': 'fries'}) == {'c': 'pepsi'}
|
||||
|
||||
def test_nested_deleted_key(self):
|
||||
# 'c' is absent from _new → None signals "delete entire key"
|
||||
assert jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hamburger', 'b': 'fries'}) == {'c': None}
|
||||
|
||||
def test_nested_partial_delete(self):
|
||||
assert jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'c': {'e': 'coke'}}) == {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}
|
||||
|
||||
def test_nested_no_delete_different_value(self):
|
||||
assert jay_diff_del({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}) == {}
|
||||
|
||||
|
||||
# ── jay_diff_upd ──────────────────────────────────────────────────────────────
|
||||
|
||||
class TestJayDiffUpd:
|
||||
def test_multiple_updates(self):
|
||||
assert jay_diff_upd({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hotdog', 'b': 'potatoes'}) == {'a': 'hotdog', 'b': 'potatoes'}
|
||||
|
||||
def test_nested_update(self):
|
||||
assert jay_diff_upd({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'c': {'e': 'pepsi'}}) == {'c': {'e': 'pepsi'}}
|
||||
|
||||
def test_one_update(self):
|
||||
assert jay_diff_upd({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hotdog', 'b': 'fries'}) == {'a': 'hotdog'}
|
||||
|
||||
def test_no_update_new_key_ignored(self):
|
||||
assert jay_diff_upd({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}) == {'c': 'coke'}
|
||||
|
||||
def test_nested_no_update_new_subkey_ignored(self):
|
||||
assert jay_diff_upd({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}},
|
||||
{'c': {'f': 'coffee'}}) == {}
|
||||
|
||||
def test_nested_update_one_of_two(self):
|
||||
assert jay_diff_upd({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': {'e': 'pepsi', 'f': 'coffee'}}) \
|
||||
== {'c': {'e': 'pepsi'}}
|
||||
|
||||
|
||||
# ── jay_diff_upd_add ──────────────────────────────────────────────────────────
|
||||
|
||||
class TestJayDiffUpdAdd:
|
||||
def test_update_and_add(self):
|
||||
assert jay_diff_upd_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hotdog', 'b': 'potatoes', 'd': 'icecream'}) \
|
||||
== {'a': 'hotdog', 'b': 'potatoes', 'd': 'icecream'}
|
||||
|
||||
def test_nested_update_and_add(self):
|
||||
assert jay_diff_upd_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}, 'b': 'potatoes'}) \
|
||||
== {'b': 'potatoes', 'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}}
|
||||
|
||||
def test_update_only(self):
|
||||
assert jay_diff_upd_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hotdog', 'b': 'fries'}) == {'a': 'hotdog'}
|
||||
|
||||
def test_nested_update_and_new_subkey(self):
|
||||
assert jay_diff_upd_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': {'e': 'pepsi', 'f': 'coffee', 'h': 'cake'}}) \
|
||||
== {'c': {'e': 'pepsi', 'h': 'cake'}}
|
||||
|
||||
def test_add_new_top_and_update(self):
|
||||
assert jay_diff_upd_add({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}) \
|
||||
== {'c': 'coke', 'd': 'icecream'}
|
||||
|
||||
def test_identical(self):
|
||||
assert jay_diff_upd_add({'a': 'hamburger', 'b': 'fries'},
|
||||
{'a': 'hamburger', 'b': 'fries'}) == {}
|
||||
|
||||
|
||||
# ── jay_diff_full (combine_upd_add=False) ─────────────────────────────────────
|
||||
|
||||
class TestJayDiffFullSeparate:
|
||||
F = False
|
||||
|
||||
def test_add_all(self):
|
||||
assert jay_diff_full({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, self.F) \
|
||||
== {'add': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}}
|
||||
|
||||
def test_update_one(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, self.F) \
|
||||
== {'update': {'c': 'coke'}}
|
||||
|
||||
def test_delete_one(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi', 'd': 'icecream'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, self.F) \
|
||||
== {'delete': {'d': 'icecream'}}
|
||||
|
||||
def test_add_delete_update(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}, self.F) \
|
||||
== {'add': {'d': 'icecream'}, 'delete': {'b': 'fries'}, 'update': {'c': 'coke'}}
|
||||
|
||||
def test_identical(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, self.F) == {}
|
||||
|
||||
def test_delete_nested(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hamburger', 'b': 'fries'}, self.F) \
|
||||
== {'delete': {'c': None}}
|
||||
|
||||
def test_nested_partial_delete(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}, self.F) \
|
||||
== {'delete': {'c': {'e': 'coke'}}}
|
||||
|
||||
def test_update_and_delete(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hotdog', 'b': 'potatoes'}, self.F) \
|
||||
== {'delete': {'c': None}, 'update': {'a': 'hotdog', 'b': 'potatoes'}}
|
||||
|
||||
def test_nested_add_delete_update(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hotdog', 'b': 'potatoes', 'd': 'icecream'}, self.F) \
|
||||
== {'add': {'d': 'icecream'}, 'delete': {'c': None}, 'update': {'a': 'hotdog', 'b': 'potatoes'}}
|
||||
|
||||
|
||||
# ── jay_diff_full (combine_upd_add=True) ──────────────────────────────────────
|
||||
|
||||
class TestJayDiffFullCombined:
|
||||
T = True
|
||||
|
||||
def test_add_all(self):
|
||||
assert jay_diff_full({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}, self.T) \
|
||||
== {'update_add': {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}}
|
||||
|
||||
def test_update_one(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, self.T) \
|
||||
== {'update_add': {'c': 'coke'}}
|
||||
|
||||
def test_delete_one(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi', 'd': 'icecream'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, self.T) \
|
||||
== {'delete': {'d': 'icecream'}}
|
||||
|
||||
def test_delete_and_update_add(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}, self.T) \
|
||||
== {'delete': {'b': 'fries'}, 'update_add': {'c': 'coke', 'd': 'icecream'}}
|
||||
|
||||
def test_identical(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, self.T) == {}
|
||||
|
||||
def test_delete_nested(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hamburger', 'b': 'fries'}, self.T) \
|
||||
== {'delete': {'c': None}}
|
||||
|
||||
def test_update_and_delete_nested(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hotdog', 'b': 'potatoes'}, self.T) \
|
||||
== {'delete': {'c': None}, 'update_add': {'a': 'hotdog', 'b': 'potatoes'}}
|
||||
|
||||
def test_nested_update_add_and_delete(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}, 'b': 'potatoes'}, self.T) \
|
||||
== {'delete': {'a': 'hamburger', 'c': {'f': 'coffee'}},
|
||||
'update_add': {'b': 'potatoes', 'c': {'e': 'pepsi', 'g': 'tea', 'h': 'cake'}}}
|
||||
|
||||
def test_nested_add_subkey(self):
|
||||
assert jay_diff_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'a': 'hamburger', 'b': 'fries', 'c': {'e': 'pepsi', 'f': 'coffee', 'h': 'cake'}}, self.T) \
|
||||
== {'update_add': {'c': {'e': 'pepsi', 'h': 'cake'}}}
|
||||
|
||||
|
||||
# ── jay_merge_* ───────────────────────────────────────────────────────────────
|
||||
|
||||
class TestJayMerge:
|
||||
def test_merge_add_to_empty(self):
|
||||
assert jay_merge_add({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
def test_merge_add_new_key(self):
|
||||
assert jay_merge_add({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'d': 'icecream'}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}, 'd': 'icecream'}
|
||||
|
||||
def test_merge_add_existing_unchanged(self):
|
||||
assert jay_merge_add({'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}, 'd': 'icecream'},
|
||||
{'d': 'icecream'}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}, 'd': 'icecream'}
|
||||
|
||||
def test_merge_delete_top_key(self):
|
||||
assert jay_merge_delete({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}, 'd': 'icecream'},
|
||||
{'d': 'icecream'}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
def test_merge_delete_nested_key(self):
|
||||
assert jay_merge_delete({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'c': {'f': 'coffee'}}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke'}}
|
||||
|
||||
def test_merge_delete_nested_leaves_sibling(self):
|
||||
assert jay_merge_delete({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'c': {'e': 'coke'}}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}}
|
||||
|
||||
|
||||
class TestJayMergeFull:
|
||||
def test_passthrough_when_old_empty(self):
|
||||
assert jay_merge_full({}, {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}}
|
||||
|
||||
def test_empty_diff_unchanged(self):
|
||||
assert jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
def test_update(self):
|
||||
assert jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'coke'}, {'update': {'c': 'pepsi'}}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}
|
||||
|
||||
def test_update_add(self):
|
||||
assert jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}, {'update_add': {'c': 'coke'}}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': 'coke'}
|
||||
|
||||
def test_delete(self):
|
||||
assert jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi', 'd': 'icecream'},
|
||||
{'delete': {'d': 'icecream'}}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'}
|
||||
|
||||
def test_add_delete_update_combined(self):
|
||||
assert jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': 'pepsi'},
|
||||
{'add': {'d': 'icecream'}, 'delete': {'b': 'fries'}, 'update': {'c': 'coke'}}) \
|
||||
== {'a': 'hamburger', 'd': 'icecream', 'c': 'coke'}
|
||||
|
||||
def test_nested_update_and_delete(self):
|
||||
assert jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'delete': {'a': 'hamburger', 'b': 'fries', 'c': {'f': 'coffee'}},
|
||||
'update': {'c': {'e': 'pepsi'}}}) \
|
||||
== {'c': {'e': 'pepsi'}}
|
||||
|
||||
def test_update_add_new_subkey(self):
|
||||
assert jay_merge_full({'a': 'hamburger', 'b': 'fries', 'c': {'e': 'coke', 'f': 'coffee'}},
|
||||
{'update_add': {'c': {'e': 'pepsi', 'h': 'cake'}}}) \
|
||||
== {'a': 'hamburger', 'b': 'fries', 'c': {'e': 'pepsi', 'f': 'coffee', 'h': 'cake'}}
|
||||
|
||||
def test_real_world_snapshot_update(self):
|
||||
old = {'charging': {
|
||||
'batteryStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:34Z', 'cruisingRangeElectric_km': 192}},
|
||||
'chargingStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:34Z'}},
|
||||
'chargingSettings': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:30Z'}},
|
||||
'plugStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:29Z'}}}}
|
||||
update = {'charging': {
|
||||
'batteryStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:35:46Z', 'cruisingRangeElectric_km': 191}},
|
||||
'chargingStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:35:46Z'}}}}
|
||||
add = {'charging': {'batteryStatus': {'value': {'currentSOC_pct': 55}}},
|
||||
'fuelStatus': {'rangeStatus': {'value': {'primaryEngine': {'currentSOC_pct': 55}}}},
|
||||
'measurements': {'fuelLevelStatus': {'value': {'currentSOC_pct': 55}}}}
|
||||
delete = {'charging': {
|
||||
'chargingSettings': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:30Z'}},
|
||||
'plugStatus': {'value': {'carCapturedTimestamp': '2024-10-19T05:34:29Z'}}}}
|
||||
result = jay_merge_full(old, {'update': update, 'add': add, 'delete': delete})
|
||||
assert result == old
|
||||
|
||||
|
||||
# ── Corner cases: data types ───────────────────────────────────────────────────
|
||||
|
||||
class TestNumericAndScalarTypes:
|
||||
def test_diff_int_value_changed(self):
|
||||
assert jay_diff_upd({'a': 1, 'b': 2}, {'a': 1, 'b': 3}) == {'b': 3}
|
||||
|
||||
def test_diff_float_value_changed(self):
|
||||
assert jay_diff_upd({'a': 1.0}, {'a': 1.1}) == {'a': 1.1}
|
||||
|
||||
def test_diff_int_unchanged(self):
|
||||
assert jay_diff_upd({'a': 42}, {'a': 42}) == {}
|
||||
|
||||
def test_diff_bool_changed(self):
|
||||
assert jay_diff_upd({'a': True}, {'a': False}) == {'a': False}
|
||||
|
||||
def test_diff_bool_false_to_true(self):
|
||||
assert jay_diff_upd({'online': False}, {'online': True}) == {'online': True}
|
||||
|
||||
def test_diff_none_value_changed(self):
|
||||
assert jay_diff_upd({'a': None}, {'a': 'something'}) == {'a': 'something'}
|
||||
|
||||
def test_diff_value_to_none(self):
|
||||
assert jay_diff_upd({'a': 'something'}, {'a': None}) == {'a': None}
|
||||
|
||||
def test_diff_list_unchanged(self):
|
||||
assert jay_diff_upd({'a': [1, 2, 3]}, {'a': [1, 2, 3]}) == {}
|
||||
|
||||
def test_diff_list_changed(self):
|
||||
assert jay_diff_upd({'a': [1, 2]}, {'a': [1, 2, 3]}) == {'a': [1, 2, 3]}
|
||||
|
||||
def test_add_numeric_key(self):
|
||||
assert jay_diff_add({'a': 1}, {'a': 1, 'b': 2}) == {'b': 2}
|
||||
|
||||
def test_full_combined_numeric(self):
|
||||
old = {'soc': 80, 'range': 300, 'temp': 22.5}
|
||||
new = {'soc': 75, 'range': 280, 'temp': 22.5}
|
||||
diff = jay_diff_full(old, new)
|
||||
assert diff == {'update_add': {'soc': 75, 'range': 280}}
|
||||
|
||||
def test_merge_numeric_update(self):
|
||||
assert jay_merge_full({'soc': 80, 'range': 300}, {'update_add': {'soc': 75}}) \
|
||||
== {'soc': 75, 'range': 300}
|
||||
|
||||
|
||||
# ── Corner cases: type changes (dict ↔ scalar) ────────────────────────────────
|
||||
|
||||
class TestTypeChanges:
|
||||
def test_dict_replaced_by_scalar_in_diff(self):
|
||||
# A key that was a dict becomes a scalar — upd treats the whole value as changed
|
||||
old = {'a': {'b': 'c'}}
|
||||
new = {'a': 'flat'}
|
||||
diff = jay_diff_upd(old, new)
|
||||
assert diff == {'a': 'flat'}
|
||||
|
||||
def test_merge_dict_to_scalar(self):
|
||||
old = {'a': {'b': 'c'}, 'x': 1}
|
||||
diff = {'update_add': {'a': 'flat'}}
|
||||
result = jay_merge_full(old, diff)
|
||||
assert result == {'a': 'flat', 'x': 1}
|
||||
|
||||
def test_diff_full_dict_to_scalar_combined(self):
|
||||
old = {'status': {'value': 42}}
|
||||
new = {'status': 'unknown'}
|
||||
diff = jay_diff_full(old, new)
|
||||
result = jay_merge_full(deepcopy(old), diff)
|
||||
assert result == new
|
||||
|
||||
def test_diff_add_new_nested_structure(self):
|
||||
# Key absent in old, present as dict in new — treated as addition
|
||||
old = {'a': 1}
|
||||
new = {'a': 1, 'b': {'c': 2}}
|
||||
diff = jay_diff_add(old, new)
|
||||
assert diff == {'b': {'c': 2}}
|
||||
|
||||
|
||||
# ── Corner cases: completely different structures ─────────────────────────────
|
||||
|
||||
class TestCompletelyDifferentStructure:
|
||||
def test_all_keys_replaced(self):
|
||||
old = {'a': 1, 'b': 2}
|
||||
new = {'x': 10, 'y': 20}
|
||||
diff = jay_diff_full(old, new)
|
||||
assert diff == {'update_add': {'x': 10, 'y': 20}, 'delete': {'a': 1, 'b': 2}}
|
||||
|
||||
def test_all_keys_replaced_deep(self):
|
||||
old = {'charging': {'state': 'charging', 'power': 11}}
|
||||
new = {'electric_drive': {'range': 300, 'soc': 80}}
|
||||
diff = jay_diff_full(old, new)
|
||||
assert diff == {
|
||||
'update_add': {'electric_drive': {'range': 300, 'soc': 80}},
|
||||
'delete': {'charging': None},
|
||||
}
|
||||
|
||||
def test_merge_round_trip_all_keys_replaced(self):
|
||||
old = {'a': 1, 'b': 2}
|
||||
new = {'x': 10, 'y': 20}
|
||||
diff = jay_diff_full(old, new)
|
||||
result = jay_merge_full(deepcopy(old), diff)
|
||||
assert result == new
|
||||
|
||||
def test_diff_disjoint_no_update(self):
|
||||
# No shared keys → no updates, only add/delete
|
||||
old = {'a': 1}
|
||||
new = {'b': 2}
|
||||
diff = jay_diff_upd(old, new)
|
||||
assert diff == {}
|
||||
|
||||
def test_diff_disjoint_add(self):
|
||||
assert jay_diff_add({'a': 1}, {'b': 2}) == {'b': 2}
|
||||
|
||||
def test_diff_disjoint_del(self):
|
||||
assert jay_diff_del({'a': 1}, {'b': 2}) == {'a': 1}
|
||||
|
||||
|
||||
# ── Corner cases: empty containers ────────────────────────────────────────────
|
||||
|
||||
class TestEmptyContainers:
|
||||
def test_diff_both_empty(self):
|
||||
assert jay_diff_full({}, {}) == {}
|
||||
|
||||
def test_diff_old_empty_new_has_data(self):
|
||||
diff = jay_diff_full({}, {'a': 1})
|
||||
assert diff == {'update_add': {'a': 1}}
|
||||
|
||||
def test_diff_new_empty_old_has_data(self):
|
||||
diff = jay_diff_full({'a': 1}, {})
|
||||
assert diff == {'delete': {'a': 1}}
|
||||
|
||||
def test_merge_delete_all_keys(self):
|
||||
result = jay_merge_full({'a': 1, 'b': 2}, {'delete': {'a': 1, 'b': 2}})
|
||||
assert result == {}
|
||||
|
||||
def test_delete_empty_dict_value(self):
|
||||
# Deleting a key whose value is {} — should not recurse (fixed bug)
|
||||
result = jay_merge_delete({'a': 'keep', 'b': {}}, {'b': {}})
|
||||
assert result == {'a': 'keep'}
|
||||
|
||||
def test_diff_nested_empty_dict(self):
|
||||
old = {'a': {'b': 'c'}}
|
||||
new = {'a': {}}
|
||||
diff = jay_diff_del(old, new)
|
||||
assert diff == {'a': {'b': 'c'}}
|
||||
|
||||
def test_empty_dict_preserved_after_delete_all_subkeys(self):
|
||||
# When new has an empty dict at a location, the key must survive with {}
|
||||
# (not be pruned away). This would break if merge incorrectly prunes empty dicts.
|
||||
old = {'x': {'p': 1, 'q': 2}}
|
||||
new = {'x': {}}
|
||||
diff = jay_diff_full(old, new)
|
||||
result = jay_merge_full(deepcopy(old), diff)
|
||||
assert result == new # 'x' must remain as {}
|
||||
|
||||
|
||||
# ── Corner cases: deep nesting ────────────────────────────────────────────────
|
||||
|
||||
class TestDeepNesting:
|
||||
def test_diff_three_levels(self):
|
||||
old = {'l1': {'l2': {'l3': 'old'}}}
|
||||
new = {'l1': {'l2': {'l3': 'new'}}}
|
||||
assert jay_diff_upd(old, new) == {'l1': {'l2': {'l3': 'new'}}}
|
||||
|
||||
def test_merge_three_levels(self):
|
||||
old = {'l1': {'l2': {'l3': 'old', 'keep': 'yes'}}}
|
||||
diff = {'update_add': {'l1': {'l2': {'l3': 'new'}}}}
|
||||
result = jay_merge_full(deepcopy(old), diff)
|
||||
assert result == {'l1': {'l2': {'l3': 'new', 'keep': 'yes'}}}
|
||||
|
||||
def test_diff_full_three_levels(self):
|
||||
old = {'a': {'b': {'c': {'d': 'deep'}}}}
|
||||
new = {'a': {'b': {'c': {'d': 'deeper'}}}}
|
||||
diff = jay_diff_full(old, new)
|
||||
result = jay_merge_full(deepcopy(old), diff)
|
||||
assert result == new
|
||||
|
||||
|
||||
# ── Round-trip: merge(old, diff(old, new)) == new ────────────────────────────
|
||||
|
||||
class TestRoundTrip:
|
||||
"""Verify that applying a diff to old always produces new."""
|
||||
|
||||
@pytest.mark.parametrize("old,new", [
|
||||
({'a': 1}, {'a': 2}),
|
||||
({'a': 1, 'b': 2}, {'b': 3, 'c': 4}),
|
||||
({'x': {'y': 'old'}}, {'x': {'y': 'new', 'z': 'added'}}),
|
||||
({'a': {'b': 1}, 'c': 2}, {'a': {'b': 1}, 'd': 3}),
|
||||
({'soc': 80, 'range': 300}, {'soc': 60, 'range': 240, 'temp': 25}),
|
||||
({'a': True, 'b': None}, {'a': False, 'c': 42}),
|
||||
({'l1': {'l2': {'v': 1}}}, {'l1': {'l2': {'v': 2}}}),
|
||||
({}, {'a': 1}),
|
||||
({'a': 1}, {}),
|
||||
])
|
||||
def test_round_trip_combined(self, old, new):
|
||||
diff = jay_diff_full(deepcopy(old), deepcopy(new), combine_upd_add=True)
|
||||
result = jay_merge_full(deepcopy(old), diff)
|
||||
assert result == new
|
||||
|
||||
@pytest.mark.parametrize("old,new", [
|
||||
({'a': 1}, {'a': 2}),
|
||||
({'a': 1, 'b': 2}, {'b': 3, 'c': 4}),
|
||||
({'x': {'y': 'old'}}, {'x': {'y': 'new', 'z': 'added'}}),
|
||||
({}, {'a': 1}),
|
||||
({'a': 1}, {}),
|
||||
])
|
||||
def test_round_trip_separate(self, old, new):
|
||||
diff = jay_diff_full(deepcopy(old), deepcopy(new), combine_upd_add=False)
|
||||
result = jay_merge_full(deepcopy(old), diff)
|
||||
assert result == new
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Round-trip tests for jay_diff using real snapshot records from ./tests/data/.
|
||||
|
||||
For every pair of consecutive records (within a file and across adjacent files)
|
||||
we verify:
|
||||
jay_merge_full(a, jay_diff_full(a, b)) == b
|
||||
|
||||
We also verify that diffing identical records produces an empty diff, and that
|
||||
applying an empty diff is a no-op.
|
||||
"""
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
from jay_diff import jay_diff_full, jay_merge_full
|
||||
|
||||
DATA_DIR = pathlib.Path(__file__).parent / "data"
|
||||
|
||||
|
||||
def _load_all_records() -> list[dict]:
|
||||
"""Return all records from all data files, sorted by timestamp."""
|
||||
records: list[dict] = []
|
||||
for path in sorted(DATA_DIR.glob("*.json")):
|
||||
store = json.loads(path.read_text())
|
||||
records.extend(store.get("records", []))
|
||||
records.sort(key=lambda r: r.get("ts", ""))
|
||||
return records
|
||||
|
||||
|
||||
ALL_RECORDS = _load_all_records()
|
||||
CONSECUTIVE_PAIRS = list(zip(ALL_RECORDS, ALL_RECORDS[1:]))
|
||||
|
||||
|
||||
class TestRealDataRoundTrip:
|
||||
"""Diff+merge of every consecutive snapshot pair must reconstruct the target."""
|
||||
|
||||
@pytest.mark.parametrize("pair_index", range(len(CONSECUTIVE_PAIRS)))
|
||||
def test_consecutive_round_trip(self, pair_index: int) -> None:
|
||||
a, b = CONSECUTIVE_PAIRS[pair_index]
|
||||
diff = jay_diff_full(a, b)
|
||||
result = jay_merge_full(a, diff)
|
||||
assert result == b, (
|
||||
f"Round-trip failed for pair #{pair_index} "
|
||||
f"(ts: {a.get('ts')} → {b.get('ts')})"
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("pair_index", range(len(CONSECUTIVE_PAIRS)))
|
||||
def test_diff_of_identical_is_empty(self, pair_index: int) -> None:
|
||||
a, _ = CONSECUTIVE_PAIRS[pair_index]
|
||||
assert jay_diff_full(a, a) == {}
|
||||
|
||||
@pytest.mark.parametrize("pair_index", range(len(CONSECUTIVE_PAIRS)))
|
||||
def test_empty_diff_is_noop(self, pair_index: int) -> None:
|
||||
a, _ = CONSECUTIVE_PAIRS[pair_index]
|
||||
assert jay_merge_full(a, {}) == a
|
||||
|
||||
|
||||
class TestRealDataProperties:
|
||||
"""Structural invariants on the real snapshot data."""
|
||||
|
||||
def test_data_dir_has_files(self) -> None:
|
||||
assert len(list(DATA_DIR.glob("*.json"))) > 0, "No JSON files in tests/data/"
|
||||
|
||||
def test_all_records_have_timestamp(self) -> None:
|
||||
for rec in ALL_RECORDS:
|
||||
assert "ts" in rec, f"Record missing 'ts': {list(rec.keys())}"
|
||||
|
||||
def test_records_are_sorted(self) -> None:
|
||||
ts_list = [r["ts"] for r in ALL_RECORDS]
|
||||
assert ts_list == sorted(ts_list), "Records are not in ascending timestamp order"
|
||||
|
||||
def test_total_record_count(self) -> None:
|
||||
assert len(ALL_RECORDS) > 100, (
|
||||
f"Expected >100 records across all files, got {len(ALL_RECORDS)}"
|
||||
)
|
||||
|
||||
def test_diff_size_smaller_than_full_snapshot(self) -> None:
|
||||
"""Diffs between similar consecutive records should usually be smaller."""
|
||||
savings = 0
|
||||
for a, b in CONSECUTIVE_PAIRS:
|
||||
diff = jay_diff_full(a, b)
|
||||
if diff: # skip identical pairs
|
||||
diff_len = len(json.dumps(diff))
|
||||
full_len = len(json.dumps(b))
|
||||
if diff_len < full_len:
|
||||
savings += 1
|
||||
ratio = savings / len(CONSECUTIVE_PAIRS) if CONSECUTIVE_PAIRS else 1.0
|
||||
assert ratio > 0.5, (
|
||||
f"Expected diffs to be smaller than full snapshots for >50% of pairs, "
|
||||
f"got {ratio:.0%}"
|
||||
)
|
||||
Reference in New Issue
Block a user