# dictutils/ops.py
from __future__ import annotations
from collections.abc import Iterable, Mapping, Sequence
from typing import Any, Callable, Union
Path = Union[str, Sequence[Union[str, int]]]
Reducer = Callable[[Any, Any], Any]
# ------------- path parsing / access primitives -------------
def _is_mapping(x: Any) -> bool:
return isinstance(x, Mapping)
def _get_attr(obj: Any, key: str, *, default: Any = None, strict: bool = False) -> Any:
if _is_mapping(obj):
if key in obj:
return obj[key]
if strict:
raise KeyError(key)
return default
# dataclass, object
if hasattr(obj, key):
return getattr(obj, key)
# allow attribute-like access on objects that expose __getitem__
if not strict:
return default
raise AttributeError(key)
def _set_attr(
obj: Any, key: str, value: Any, *, create_mapping: Callable[[], Any]
) -> None:
if _is_mapping(obj):
obj[key] = value
return
# if object, try setattr
try:
setattr(obj, key, value)
except Exception:
# fallback: if it looks like a mapping-like thing, try __setitem__
if hasattr(obj, "__setitem__"):
obj[key] = value
else:
raise
def _del_attr(obj: Any, key: str) -> None:
if _is_mapping(obj):
del obj[key]
return
if hasattr(obj, key):
delattr(obj, key)
return
if hasattr(obj, "__delitem__"):
del obj[key]
return
raise KeyError(key)
def _is_int(s: str) -> bool:
return s.isdigit() or (s.startswith("-") and s[1:].isdigit())
def _parse_path(path: Path) -> list[str | int]:
"""
Parse 'a.b[0].c' or ['a','b',0,'c'] into ['a','b',0,'c'].
"""
if isinstance(path, (list, tuple)):
return list(path)
s = str(path)
out: list[str | int] = []
buf: list[str] = []
i = 0
while i < len(s):
ch = s[i]
if ch == ".":
if buf:
tok = "".join(buf)
out.append(int(tok) if _is_int(tok) else tok)
buf = []
i += 1
elif ch == "[":
# flush current
if buf:
tok = "".join(buf)
out.append(int(tok) if _is_int(tok) else tok)
buf = []
j = s.find("]", i + 1)
if j == -1:
raise ValueError(f"Unmatched '[' in path: {s}")
idx = s[i + 1 : j]
out.append(int(idx) if _is_int(idx) else idx)
i = j + 1
else:
buf.append(ch)
i += 1
if buf:
tok = "".join(buf)
out.append(int(tok) if _is_int(tok) else tok)
return out
def _ensure_container(
parent: Any, key: str | int, *, create_mapping: Callable[[], Any]
) -> Any:
"""
Ensure parent[key] exists and is a mapping when key is str,
or a list when key is int. Returns the child container.
"""
if isinstance(key, int):
# list access
if parent is None:
raise TypeError("Cannot index None with int")
if isinstance(parent, list):
# extend list
while len(parent) <= key if key >= 0 else False:
parent.append(None)
if key < 0:
raise IndexError("negative indices not supported for creation")
if parent[key] is None:
parent[key] = create_mapping()
return parent[key]
# auto-create list if mapping slot is missing
if _is_mapping(parent):
lst: list[Any] = []
parent[str(key)] = lst # store under stringified index to avoid collisions
while len(lst) <= key:
lst.append(None)
lst[key] = create_mapping()
return lst[key]
raise TypeError(f"Cannot ensure list on parent type {type(parent)}")
# string key -> mapping-like
if parent is None:
raise TypeError("Cannot set on None")
if _is_mapping(parent):
if key not in parent or parent[key] is None:
parent[key] = create_mapping()
return parent[key]
# object
cur = getattr(parent, key, None)
if cur is None:
cur = create_mapping()
_set_attr(parent, key, cur, create_mapping=create_mapping)
return cur
# ------------- deep_get / set / del / has -------------
[docs]
def deep_get(obj: Any, path: Path, default: Any = None, *, strict: bool = False) -> Any:
"""
Get value from nested object using dotted path notation or path sequence.
Args:
obj: The object to access
path: Path as string ("a.b.c", "a[0].b") or sequence ["a", "b", 0]
default: Value to return if path not found (when strict=False)
strict: If True, raise exception when path not found
Returns:
Value at the specified path, or default if not found
Example:
>>> import json
>>> data = {"user": {"profile": {"name": "Alice", "tags": ["admin", "user"]}}}
>>> result = deep_get(data, "user.profile.name")
>>> print(json.dumps(result, indent=4))
"Alice"
>>> result = deep_get(data, ["user", "profile", "tags", 0])
>>> print(json.dumps(result, indent=4))
"admin"
>>> result = deep_get(data, "user.profile.age", default="unknown")
>>> print(json.dumps(result, indent=4))
"unknown"
"""
parts = _parse_path(path)
cur = obj
for p in parts:
if isinstance(p, int):
if isinstance(cur, list) and 0 <= p < len(cur):
cur = cur[p]
elif _is_mapping(cur) and str(p) in cur:
cur = cur[str(p)]
else:
if strict:
raise KeyError(p)
return default
else:
cur = _get_attr(cur, p, default=default, strict=strict)
if cur is default and not strict:
return default
return cur
[docs]
def deep_has(obj: Any, path: Path) -> bool:
"""
Check if a path exists in nested object.
Args:
obj: The object to check
path: Path as string ("a.b.c") or sequence ["a", "b", "c"]
Returns:
True if path exists, False otherwise
Example:
>>> import json
>>> data = {"user": {"profile": {"name": "Alice"}}}
>>> result = deep_has(data, "user.profile.name")
>>> print(json.dumps(result, indent=4))
true
>>> result = deep_has(data, "user.profile.age")
>>> print(json.dumps(result, indent=4))
false
"""
try:
sentinel = object()
return deep_get(obj, path, default=sentinel, strict=True) is not sentinel
except Exception:
return False
[docs]
def deep_set(
obj: Any,
path: Path,
value: Any,
*,
create_missing: bool = True,
create_mapping: Callable[[], Any] = dict,
) -> Any:
"""
Set value in nested object using dotted path notation.
Args:
obj: The object to modify
path: Path as string ("a.b.c") or sequence ["a", "b", "c"]
value: Value to set at the path
create_missing: Whether to create missing intermediate paths
create_mapping: Factory function for creating new mappings
Returns:
The modified object
Example:
>>> import json
>>> data = {}
>>> deep_set(data, "user.profile.name", "Alice")
>>> print(json.dumps(data, indent=4))
{
"user": {
"profile": {
"name": "Alice"
}
}
}
>>> deep_set(data, "user.profile.tags[0]", "admin")
>>> print(json.dumps(data, indent=4))
{
"user": {
"profile": {
"name": "Alice",
"tags": [
"admin"
]
}
}
}
"""
parts = _parse_path(path)
if not parts:
return obj
cur = obj
for i, p in enumerate(parts[:-1]):
nxt = parts[i + 1]
# navigate or create
if isinstance(p, int):
if not isinstance(cur, list):
if not create_missing:
raise TypeError(f"Expected list at segment {p}")
# create list in a mapping slot
raise TypeError(
"Cannot auto-create a list container at numeric segment without parent setter"
)
# ensure index exists
while len(cur) <= p:
cur.append(None)
if cur[p] is None:
cur[p] = (
dict()
if isinstance(nxt, (str,))
else ([] if isinstance(nxt, int) else dict())
)
cur = cur[p]
elif create_missing:
cur = _ensure_container(cur, p, create_mapping=create_mapping)
else:
cur = _get_attr(cur, p, strict=True)
# set leaf
last = parts[-1]
if isinstance(last, int):
if not isinstance(cur, list):
if create_missing and _is_mapping(cur):
# store list at stringified index
lst: list[Any] = []
cur[str(last)] = lst
while len(lst) <= last:
lst.append(None)
lst[last] = value
return obj
raise TypeError("Expected list at final segment")
while len(cur) <= last:
cur.append(None)
cur[last] = value
return obj
# string key
if _is_mapping(cur):
cur[last] = value
else:
_set_attr(cur, last, value, create_mapping=create_mapping)
return obj
[docs]
def deep_del(obj: Any, path: Path) -> Any:
"""
Delete value from nested object using dotted path notation.
Args:
obj: The object to modify
path: Path as string ("a.b.c") or sequence ["a", "b", "c"]
Returns:
The modified object
Example:
>>> import json
>>> data = {"user": {"profile": {"name": "Alice", "age": 30}}}
>>> deep_del(data, "user.profile.age")
>>> print(json.dumps(data, indent=4))
{
"user": {
"profile": {
"name": "Alice"
}
}
}
"""
parts = _parse_path(path)
if not parts:
return obj
parent = deep_get(obj, parts[:-1], strict=True)
last = parts[-1]
if isinstance(last, int):
if not isinstance(parent, list) or not (0 <= last < len(parent)):
raise KeyError(last)
del parent[last]
else:
_del_attr(parent, last)
return obj
# ------------- deep_update / deep_diff -------------
[docs]
def deep_update(
a: Any,
b: Any,
*,
dict_strategy: str = "merge", # "merge" | "replace"
list_strategy: str = "extend", # "extend" | "replace" | "unique" | "by_key"
unique_by: Callable[[Any], Any] | None = None,
by_key: str | Callable[[Any], Any] | None = None,
scalar_strategy: str = "replace", # "replace" | "keep_first" | "keep_last"
) -> Any:
"""
Strategy-aware deep update of object a with values from object b.
Args:
a: Target object to update
b: Source object to merge from
dict_strategy: How to handle dict merging ("merge" or "replace")
list_strategy: How to handle list merging ("extend", "replace", "unique", "by_key")
unique_by: Function to extract unique key for "unique" strategy
by_key: Key or function for "by_key" strategy
scalar_strategy: How to handle scalar conflicts ("replace", "keep_first", "keep_last")
Returns:
The updated object a
Example:
>>> import json
>>> data1 = {"users": [{"id": 1, "name": "Alice"}], "config": {"debug": True}}
>>> data2 = {"users": [{"id": 2, "name": "Bob"}], "config": {"port": 8080}}
>>> deep_update(data1, data2)
>>> print(json.dumps(data1, indent=4))
{
"users": [
{
"id": 1,
"name": "Alice"
},
{
"id": 2,
"name": "Bob"
}
],
"config": {
"debug": true,
"port": 8080
}
}
"""
if a is None:
return b
if b is None:
return a
# dict vs dict
if _is_mapping(a) and _is_mapping(b):
if dict_strategy == "replace":
a.clear()
a.update(b)
return a
# merge
for k, vb in b.items():
if k in a:
a[k] = deep_update(
a[k],
vb,
dict_strategy=dict_strategy,
list_strategy=list_strategy,
unique_by=unique_by,
by_key=by_key,
scalar_strategy=scalar_strategy,
)
else:
a[k] = vb
return a
# list vs list
if isinstance(a, list) and isinstance(b, list):
if list_strategy == "replace":
a[:] = b
return a
if list_strategy == "extend":
a.extend(b)
return a
if list_strategy == "unique":
key = unique_by or (lambda x: x)
seen = {key(x) for x in a}
for x in b:
kx = key(x)
if kx not in seen:
a.append(x)
seen.add(kx)
return a
if list_strategy == "by_key":
if by_key is None:
raise ValueError("by_key is required for list_strategy='by_key'")
getk = (lambda x: x.get(by_key)) if isinstance(by_key, str) else by_key
idx = {getk(x): i for i, x in enumerate(a)}
for el in b:
k = getk(el)
if (
k in idx
and isinstance(a[idx[k]], Mapping)
and isinstance(el, Mapping)
):
a[idx[k]] = deep_update(
a[idx[k]],
el,
dict_strategy=dict_strategy,
list_strategy=list_strategy,
unique_by=unique_by,
by_key=by_key,
scalar_strategy=scalar_strategy,
)
else:
a.append(el)
return a
# scalars / mismatched types
if scalar_strategy in ("replace", "keep_last"):
return b
if scalar_strategy == "keep_first":
return a
return b
[docs]
def deep_diff(a: Any, b: Any) -> tuple[Any, Any, Any, Any]:
"""
Compare two nested structures and return differences.
Args:
a: First object to compare
b: Second object to compare
Returns:
Tuple of (added, removed, changed, same) where each mirrors the input structure
Example:
>>> import json
>>> data1 = {"user": {"name": "Alice", "age": 30}, "active": True}
>>> data2 = {"user": {"name": "Alice", "age": 31}, "role": "admin"}
>>> added, removed, changed, same = deep_diff(data1, data2)
>>> print(json.dumps({"added": added, "removed": removed, "changed": changed}, indent=4))
{
"added": {
"role": "admin"
},
"removed": {
"active": true
},
"changed": {
"user": {
"changed": {
"age": {
"from": 30,
"to": 31
}
},
"same": {
"name": "Alice"
}
}
}
}
"""
if _is_mapping(a) and _is_mapping(b):
akeys = set(a.keys())
bkeys = set(b.keys())
added = {k: b[k] for k in bkeys - akeys}
removed = {k: a[k] for k in akeys - bkeys}
changed: dict[str, Any] = {}
same: dict[str, Any] = {}
for k in akeys & bkeys:
sub = deep_diff(a[k], b[k])
if any(x not in ({}, []) and x is not None for x in sub[:3]):
# something changed beneath
added_k, removed_k, changed_k, same_k = sub
node: dict[str, Any] = {}
if added_k not in ({}, []) and added_k is not None:
node["added"] = added_k
if removed_k not in ({}, []) and removed_k is not None:
node["removed"] = removed_k
if changed_k not in ({}, []) and changed_k is not None:
node["changed"] = changed_k
if same_k not in ({}, []) and same_k is not None:
node["same"] = same_k
changed[k] = node
elif a[k] == b[k]:
same[k] = a[k]
else:
changed[k] = {"from": a[k], "to": b[k]}
return (added, removed, changed, same)
if isinstance(a, list) and isinstance(b, list):
# simple list diff (by position)
maxlen = max(len(a), len(b))
added_list: list[Any] = []
removed_list: list[Any] = []
changed_list: list[dict[str, Any]] = []
same_list: list[Any] = []
for i in range(maxlen):
if i >= len(a):
added_list.append(b[i])
continue
if i >= len(b):
removed_list.append(a[i])
continue
if a[i] == b[i]:
same_list.append(a[i])
else:
changed_list.append({"from": a[i], "to": b[i]})
return (added_list, removed_list, changed_list, same_list)
# scalars
if a == b:
return ({}, {}, {}, a)
return (b, a, {"from": a, "to": b}, {})
# ------------- flatten/expand/select/rename/transpose -------------
[docs]
def flatten_paths(d: Any, prefix: str = "", sep: str = ".") -> dict[str, Any]:
"""
Flatten nested mappings into dot-notation paths.
Args:
d: Object to flatten
prefix: Prefix for all paths
sep: Separator for path components
Returns:
Dictionary with flattened paths as keys
Example:
>>> import json
>>> data = {"user": {"profile": {"name": "Alice", "tags": ["admin", "user"]}}}
>>> result = flatten_paths(data)
>>> print(json.dumps(result, indent=4))
{
"user.profile.name": "Alice",
"user.profile.tags[0]": "admin",
"user.profile.tags[1]": "user"
}
"""
out: dict[str, Any] = {}
def _walk(x: Any, cur: str) -> None:
if _is_mapping(x):
for k, v in x.items():
key = f"{cur}{sep}{k}" if cur else str(k)
_walk(v, key)
elif isinstance(x, list):
for i, v in enumerate(x):
key = f"{cur}[{i}]"
_walk(v, key)
else:
out[cur] = x
_walk(d, prefix or "")
return out
[docs]
def expand_paths(path: str | Sequence[str | int]) -> list[str | int]:
"""
Parse dotted path string into list of keys.
Args:
path: Dotted path string or sequence
Returns:
List of path components
Examples:
>>> expand_paths("a.b.c")
['a', 'b', 'c']
>>> expand_paths("users.0.name")
['users', 0, 'name']
"""
if isinstance(path, (list, tuple)):
return list(path)
s = str(path)
out: list[str | int] = []
buf: list[str] = []
i = 0
while i < len(s):
ch = s[i]
if ch == ".":
if buf:
tok = "".join(buf)
out.append(int(tok) if _is_int(tok) else tok)
buf = []
i += 1
elif ch == "[":
# flush current
if buf:
tok = "".join(buf)
out.append(int(tok) if _is_int(tok) else tok)
buf = []
j = s.find("]", i + 1)
if j == -1:
raise ValueError(f"Unmatched '[' in path: {s}")
idx = s[i + 1 : j]
out.append(int(idx) if _is_int(idx) else idx)
i = j + 1
else:
buf.append(ch)
i += 1
if buf:
tok = "".join(buf)
out.append(int(tok) if _is_int(tok) else tok)
return out
[docs]
def project(d: Any, paths: Iterable[Path]) -> dict[str, Any]:
"""
Extract only specified paths from nested object.
Args:
d: Source object to project from
paths: Paths to extract
Returns:
New object containing only the specified paths
Example:
>>> import json
>>> data = {"user": {"name": "Alice", "age": 30, "email": "alice@example.com"}}
>>> result = project(data, ["user.name", "user.email"])
>>> print(json.dumps(result, indent=4))
{
"user": {
"name": "Alice",
"email": "alice@example.com"
}
}
"""
out: dict[str, Any] = {}
for p in paths:
val = deep_get(d, p, default=None, strict=False)
deep_set(out, p, val)
return out
[docs]
def rename_keys(d: Any, mapping: Mapping[str, str]) -> Any:
"""
Rename keys using path mapping.
Args:
d: Object to modify
mapping: Dictionary mapping old paths to new paths
Returns:
The modified object
Example:
>>> import json
>>> data = {"user": {"firstName": "Alice", "lastName": "Smith"}}
>>> rename_keys(data, {
... "user.firstName": "user.name.first",
... "user.lastName": "user.name.last"
... })
>>> print(json.dumps(data, indent=4))
{
"user": {
"name": {
"first": "Alice",
"last": "Smith"
}
}
}
"""
for old, new in mapping.items():
if deep_has(d, old):
val = deep_get(d, old)
deep_del(d, old)
deep_set(d, new, val)
return d
[docs]
def transpose_dict(d: Mapping[str, Mapping[str, Any]]) -> dict[str, dict[str, Any]]:
"""
Transpose a 2D dictionary (swap row/column keys).
Args:
d: Dictionary of dictionaries to transpose
Returns:
Transposed dictionary
Example:
>>> import json
>>> data = {"row1": {"col1": "A", "col2": "B"}, "row2": {"col1": "C", "col2": "D"}}
>>> result = transpose_dict(data)
>>> print(json.dumps(result, indent=4))
{
"col1": {
"row1": "A",
"row2": "C"
},
"col2": {
"row1": "B",
"row2": "D"
}
}
"""
out: dict[str, dict[str, Any]] = {}
for rkey, inner in d.items():
for ckey, val in inner.items():
out.setdefault(ckey, {})[rkey] = val
return out
# ------------- indexing / grouping / reductions -------------
[docs]
def index_by(items: Iterable[Any], key: str | Callable[[Any], Any]) -> dict[Any, Any]:
"""
Create dictionary indexed by key function or path.
Args:
items: Collection to index
key: Path string or function to extract index key
Returns:
Dictionary mapping keys to items
Example:
>>> import json
>>> users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
>>> result = index_by(users, "id")
>>> print(json.dumps(result, indent=4))
{
"1": {
"id": 1,
"name": "Alice"
},
"2": {
"id": 2,
"name": "Bob"
}
}
"""
def getk(x):
return deep_get(x, key) if isinstance(key, str) else key(x)
out: dict[Any, Any] = {}
for it in items:
out[getk(it)] = it
return out
[docs]
def group_by(
items: Iterable[Any], key: str | Callable[[Any], Any]
) -> dict[Any, list[Any]]:
"""
Group items by key function or path.
Args:
items: Collection to group
key: Path string or function to extract grouping key
Returns:
Dictionary mapping keys to lists of items
Example:
>>> import json
>>> users = [
... {"role": "admin", "name": "Alice"},
... {"role": "user", "name": "Bob"},
... {"role": "admin", "name": "Carol"}
... ]
>>> result = group_by(users, "role")
>>> print(json.dumps(result, indent=4))
{
"admin": [
{
"role": "admin",
"name": "Alice"
},
{
"role": "admin",
"name": "Carol"
}
],
"user": [
{
"role": "user",
"name": "Bob"
}
]
}
"""
def getk(x):
return deep_get(x, key) if isinstance(key, str) else key(x)
out: dict[Any, list[Any]] = {}
for it in items:
out.setdefault(getk(it), []).append(it)
return out
[docs]
def count_by(items: Iterable[Any], key: str | Callable[[Any], Any]) -> dict[Any, int]:
"""
Count items by key function or path.
Args:
items: Collection to count
key: Path string or function to extract counting key
Returns:
Dictionary mapping keys to counts
Example:
>>> import json
>>> users = [{"role": "admin"}, {"role": "user"}, {"role": "admin"}, {"role": "guest"}]
>>> result = count_by(users, "role")
>>> print(json.dumps(result, indent=4))
{
"admin": 2,
"user": 1,
"guest": 1
}
"""
def getk(x):
return deep_get(x, key) if isinstance(key, str) else key(x)
out: dict[Any, int] = {}
for it in items:
k = getk(it)
out[k] = out.get(k, 0) + 1
return out
[docs]
def sum_by(
items: Iterable[Any],
key: str | Callable[[Any], Any],
map: Callable[[Any], Any],
) -> dict[Any, Any]:
"""
Sum mapped values by key function or path.
Args:
items: Collection to process
key: Path string or function to extract grouping key
map: Function to extract value to sum
Returns:
Dictionary mapping keys to summed values
Example:
>>> import json
>>> sales = [
... {"region": "North", "amount": 100},
... {"region": "South", "amount": 200},
... {"region": "North", "amount": 150}
... ]
>>> result = sum_by(sales, "region", lambda x: x["amount"])
>>> print(json.dumps(result, indent=4))
{
"North": 250,
"South": 200
}
"""
def getk(x):
return deep_get(x, key) if isinstance(key, str) else key(x)
out: dict[Any, Any] = {}
for it in items:
k = getk(it)
v = map(it)
out[k] = (out.get(k, 0) + v) if v is not None else out.get(k, 0)
return out
[docs]
def reduce_by(
items: Iterable[Any],
key: str | Callable[[Any], Any],
*,
zero: Any,
reduce: Reducer,
map: Callable[[Any], Any] = lambda x: x,
) -> dict[Any, Any]:
"""
Reduce items by key using custom reducer function.
Args:
items: Collection to reduce
key: Path string or function to extract grouping key
zero: Initial value for reduction
reduce: Binary function to combine values
map: Function to transform items before reduction
Returns:
Dictionary mapping keys to reduced values
Example:
>>> import json
>>> sales = [
... {"region": "North", "amount": 100},
... {"region": "South", "amount": 200},
... {"region": "North", "amount": 150}
... ]
>>> result = reduce_by(sales, "region", zero=0, reduce=lambda a, b: max(a, b),
... map=lambda x: x["amount"])
>>> print(json.dumps(result, indent=4))
{
"North": 150,
"South": 200
}
"""
def getk(x):
return deep_get(x, key) if isinstance(key, str) else key(x)
out: dict[Any, Any] = {}
for it in items:
k = getk(it)
v = map(it)
out[k] = reduce(out.get(k, zero), v)
return out
[docs]
def rollup_tree(
tree: Mapping[str, Any], reducers: Mapping[str, Callable[[Any, Any], Any]]
) -> dict[str, Any]:
"""
Roll up aggregate values from leaves to parents in a tree structure.
Args:
tree: Tree structure as nested dictionaries
reducers: Map of field names to reduction functions
Returns:
Tree with rolled-up values
Example:
>>> import json
>>> tree = {
... "departments": {
... "engineering": {
... "budget": 100,
... "teams": {
... "frontend": {"budget": 40},
... "backend": {"budget": 60}
... }
... }
... }
... }
>>> result = rollup_tree(tree, {"budget": lambda a, b: (a or 0) + (b or 0)})
>>> print(json.dumps(result, indent=4))
{
"departments": {
"engineering": {
"budget": 200,
"teams": {
"frontend": {
"budget": 40
},
"backend": {
"budget": 60
}
}
}
}
}
"""
def _walk(node: dict[str, Any]) -> dict[str, Any]:
totals = {name: None for name in reducers}
for _k, v in node.items():
if isinstance(v, dict):
child_totals = _walk(v)
for name, red in reducers.items():
if child_totals.get(name) is not None:
totals[name] = (
red(totals[name], child_totals[name])
if totals[name] is not None
else child_totals[name]
)
# add current node's own values
for name, red in reducers.items():
if name in node and node[name] is not None:
totals[name] = (
red(totals[name], node[name])
if totals[name] is not None
else node[name]
)
# write back
for name, val in totals.items():
if val is not None:
node[name] = val
return totals
root = dict(tree)
_walk(root)
return root
# ------------- transform & validation -------------
[docs]
def map_values(
d: Any,
fn: Callable[[Any], Any],
*,
deep: bool = False,
predicate: Callable[[Any, Any], bool] | None = None,
) -> Any:
"""
Transform values in a mapping using a function.
Args:
d: Object to transform
fn: Function to apply to values
deep: Whether to recursively transform nested mappings
predicate: Optional function to filter which values to transform
Returns:
New object with transformed values
Example:
>>> import json
>>> data = {"user": {"age": 30, "score": 95.5, "name": "Alice"}}
>>> result = map_values(data, lambda x: x * 2 if isinstance(x, (int, float)) else x,
... deep=True)
>>> print(json.dumps(result, indent=4))
{
"user": {
"age": 60,
"score": 191.0,
"name": "Alice"
}
}
"""
if not _is_mapping(d):
return fn(d) if (predicate is None or predicate(None, d)) else d
out: dict[Any, Any] = {}
for k, v in d.items():
if deep and _is_mapping(v):
out[k] = map_values(v, fn, deep=True, predicate=predicate)
else:
out[k] = fn(v) if (predicate is None or predicate(k, v)) else v
return out
[docs]
def map_keys(
d: Mapping[str, Any],
fn: Callable[[str], str],
*,
deep: bool = False,
) -> dict[str, Any]:
"""
Transform keys in a mapping using a function.
Args:
d: Mapping to transform
fn: Function to apply to keys
deep: Whether to recursively transform nested mappings
Returns:
New mapping with transformed keys
Example:
>>> import json
>>> data = {
... "first_name": "Alice",
... "last_name": "Smith",
... "user_details": {"email_address": "alice@example.com"}
... }
>>> result = map_keys(data, lambda k: k.replace("_", "-"), deep=True)
>>> print(json.dumps(result, indent=4))
{
"first-name": "Alice",
"last-name": "Smith",
"user-details": {
"email-address": "alice@example.com"
}
}
"""
out: dict[str, Any] = {}
for k, v in d.items():
nk = fn(k)
out[nk] = map_keys(v, fn, deep=True) if deep and _is_mapping(v) else v
return out
[docs]
def map_items(
d: Mapping[str, Any],
fn: Callable[[str, Any], tuple[str, Any]],
*,
deep: bool = False,
) -> dict[str, Any]:
"""
Transform both keys and values in a mapping using a function.
Args:
d: Mapping to transform
fn: Function that takes (key, value) and returns (new_key, new_value)
deep: Whether to recursively transform nested mappings
Returns:
New mapping with transformed items
Example:
>>> import json
>>> data = {"count": 5, "total": 100}
>>> result = map_items(data, lambda k, v: (f"{k}_value", v * 10))
>>> print(json.dumps(result, indent=4))
{
"count_value": 50,
"total_value": 1000
}
"""
out: dict[str, Any] = {}
for k, v in d.items():
if deep and _is_mapping(v):
mapped_v = map_items(v, fn, deep=True)
else:
mapped_v = v
nk, nv = fn(k, mapped_v)
out[nk] = nv
return out
[docs]
def schema_check(
d: Any, schema: Mapping[str, Any], *, mode: str = "collect"
) -> list[str]:
"""
Validate object against a simple schema.
Args:
d: Object to validate
schema: Schema definition with nested structure and types
mode: "collect" to return errors, "raise" to throw exception
Returns:
List of validation error messages
Example:
>>> import json
>>> data = {"user": {"name": "Alice", "age": "30"}} # age should be int
>>> schema = {"user": {"name": str, "age": int}}
>>> result = schema_check(data, schema)
>>> print(json.dumps(result, indent=4))
[
"user.age: expected <class 'int'>, got <class 'str'>"
]
"""
errors: list[str] = []
def _walk(node: Any, sch: Any, path: str) -> None:
if _is_mapping(sch):
if not _is_mapping(node):
errors.append(f"{path or '<root>'}: expected mapping")
return
for k, sub in sch.items():
if k not in node:
errors.append(f"{path + '.' if path else ''}{k}: missing")
else:
_walk(node[k], sub, f"{path + '.' if path else ''}{k}")
elif sch is not None and not isinstance(node, sch):
errors.append(f"{path}: expected {sch}, got {type(node)}")
_walk(d, schema, "")
if errors and mode == "raise":
raise ValueError("schema_check failed:\n" + "\n".join(errors))
return errors
[docs]
def coalesce_paths(
d: Any,
candidates: Sequence[Path],
*,
set_to: Path | None = None,
default: Any = None,
) -> Any:
"""
Return first existing value from candidate paths, optionally setting result to a path.
Args:
d: Object to search
candidates: List of paths to try in order
set_to: Optional path to set the found value to
default: Value to return if no candidates exist
Returns:
First found value or default
Example:
>>> import json
>>> data = {"user": {"email": "alice@example.com"}}
>>> result = coalesce_paths(data, ["user.username", "user.email", "user.id"],
... default="anonymous")
>>> print(json.dumps(result, indent=4))
"alice@example.com"
>>> coalesce_paths(data, ["user.username", "user.id"],
... set_to="user.display_name", default="anonymous")
>>> print(json.dumps(data, indent=4))
{
"user": {
"email": "alice@example.com",
"display_name": "anonymous"
}
}
"""
for p in candidates:
if deep_has(d, p):
val = deep_get(d, p)
if set_to is not None:
deep_set(d, set_to, val)
return val
if set_to is not None:
deep_set(d, set_to, default)
return default
[docs]
def prune(
d: Any,
*,
remove_empty: bool = True,
predicate: Callable[[Any, Any], bool] | None = None,
) -> Any:
"""
Remove empty values or values matching predicate, recursively.
Args:
d: Object to prune
remove_empty: Whether to remove None, {}, [] values
predicate: Optional function to determine what to remove
Returns:
Pruned object
Example:
>>> import json
>>> data = {
... "user": {
... "name": "Alice",
... "email": None,
... "tags": [],
... "profile": {"bio": "", "settings": {}}
... }
... }
>>> result = prune(data)
>>> print(json.dumps(result, indent=4))
{
"user": {
"name": "Alice"
}
}
"""
if _is_mapping(d):
keys = list(d.keys())
for k in keys:
v = prune(d[k], remove_empty=remove_empty, predicate=predicate)
drop = False
if predicate and predicate(k, v):
drop = True
if remove_empty and (v is None or v in ({}, [])):
drop = True
if drop:
del d[k]
else:
d[k] = v
return d
if isinstance(d, list):
out = [prune(x, remove_empty=remove_empty, predicate=predicate) for x in d]
return [x for x in out if not (remove_empty and (x is None or x in ({}, [])))]
return d
# ------------- merging lists / patching -------------
[docs]
def merge_lists_by(
a: list[Any],
b: list[Any],
*,
key: str | Callable[[Any], Any],
on_conflict: str = "prefer_right", # "prefer_right" | "prefer_left" | "merge_dict"
) -> list[Any]:
"""
Merge two lists by matching items on a key.
Args:
a: First list (modified in place)
b: Second list to merge from
key: Path or function to extract matching key
on_conflict: How to handle conflicts ("prefer_right", "prefer_left", "merge_dict")
Returns:
The merged list a
Example:
>>> import json
>>> users1 = [{"id": 1, "name": "Alice", "role": "user"}]
>>> users2 = [
... {"id": 1, "name": "Alice", "role": "admin"},
... {"id": 2, "name": "Bob", "role": "user"}
... ]
>>> result = merge_lists_by(users1, users2, key="id")
>>> print(json.dumps(result, indent=4))
[
{
"id": 1,
"name": "Alice",
"role": "admin"
},
{
"id": 2,
"name": "Bob",
"role": "user"
}
]
"""
def getk(x):
return deep_get(x, key) if isinstance(key, str) else key(x)
idx = {getk(x): i for i, x in enumerate(a)}
for el in b:
k = getk(el)
if k in idx:
i = idx[k]
if on_conflict == "prefer_left":
continue
if on_conflict == "prefer_right":
a[i] = el
continue
if on_conflict == "merge_dict" and _is_mapping(a[i]) and _is_mapping(el):
a[i] = deep_update(a[i], el) # default strategies
else:
a[i] = el
else:
a.append(el)
idx[k] = len(a) - 1
return a
[docs]
def patch(d: Any, ops: Sequence[Mapping[str, Any]]) -> Any:
"""
Apply JSON-Patch-like operations to an object.
Args:
d: Object to patch
ops: List of operation dictionaries with "op", "path", and optionally "value"
Returns:
The patched object
Example:
>>> import json
>>> data = {"user": {"name": "Alice", "age": 30}}
>>> operations = [
... {"op": "replace", "path": "user.age", "value": 31},
... {"op": "add", "path": "user.email", "value": "alice@example.com"},
... {"op": "remove", "path": "user.age"}
... ]
>>> result = patch(data, operations)
>>> print(json.dumps(result, indent=4))
{
"user": {
"name": "Alice",
"email": "alice@example.com"
}
}
"""
for op in ops:
operation = op.get("op")
path = op.get("path")
if operation in ("add", "replace"):
if path is not None:
deep_set(d, path, op.get("value"))
elif operation == "remove":
if path is not None:
deep_del(d, path)
else:
raise ValueError(f"Unsupported op: {operation}")
return d
# ------------- query-ish -------------
[docs]
def where(items: Iterable[Any], pred: Callable[[Any], bool]) -> list[Any]:
"""
Filter items by predicate function.
Args:
items: Collection to filter
pred: Function that returns True for items to keep
Returns:
List of items matching the predicate
Example:
>>> import json
>>> users = [
... {"name": "Alice", "age": 30},
... {"name": "Bob", "age": 25},
... {"name": "Carol", "age": 35}
... ]
>>> result = where(users, lambda x: x["age"] >= 30)
>>> print(json.dumps(result, indent=4))
[
{
"name": "Alice",
"age": 30
},
{
"name": "Carol",
"age": 35
}
]
"""
return [x for x in items if pred(x)]
[docs]
def match(items: Iterable[Any], **eq: Any) -> list[Any]:
"""
Match items by exact field values using dot-path notation.
Args:
items: Collection to search
**eq: Field paths and expected values
Returns:
List of items matching all criteria
Example:
>>> import json
>>> users = [
... {"name": "Alice", "profile": {"role": "admin", "active": True}},
... {"name": "Bob", "profile": {"role": "user", "active": True}},
... {"name": "Carol", "profile": {"role": "admin", "active": False}}
... ]
>>> result = match(users, **{"profile.role": "admin", "profile.active": True})
>>> print(json.dumps(result, indent=4))
[
{
"name": "Alice",
"profile": {
"role": "admin",
"active": true
}
}
]
"""
outs: list[Any] = []
for it in items:
ok = True
for p, v in eq.items():
if deep_get(it, p, default=None) != v:
ok = False
break
if ok:
outs.append(it)
return outs
[docs]
def distinct_by(items: Iterable[Any], key: str | Callable[[Any], Any]) -> list[Any]:
"""
Remove duplicates by key function or path.
Args:
items: Collection to deduplicate
key: Path string or function to extract uniqueness key
Returns:
List with duplicates removed (first occurrence kept)
Example:
>>> import json
>>> users = [
... {"id": 1, "name": "Alice"},
... {"id": 2, "name": "Bob"},
... {"id": 1, "name": "Alice Updated"}
... ]
>>> result = distinct_by(users, "id")
>>> print(json.dumps(result, indent=4))
[
{
"id": 1,
"name": "Alice"
},
{
"id": 2,
"name": "Bob"
}
]
"""
def getk(x):
return deep_get(x, key) if isinstance(key, str) else key(x)
seen = set()
out: list[Any] = []
for it in items:
k = getk(it)
if k not in seen:
seen.add(k)
out.append(it)
return out
# ------------- ensure / ensure_path -------------
[docs]
def ensure_path(
d: Any,
path: Path,
*,
factory: Callable[[], Any] = dict,
) -> Any:
"""
Ensure nested path exists and return the container at that path.
Args:
d: Object to modify
path: Path to ensure exists
factory: Function to create new containers
Returns:
The container at the specified path
Example:
>>> import json
>>> data = {}
>>> container = ensure_path(data, "user.profile.settings")
>>> container["theme"] = "dark"
>>> print(json.dumps(data, indent=4))
{
"user": {
"profile": {
"settings": {
"theme": "dark"
}
}
}
}
"""
parts = _parse_path(path)
cur = d
for i, p in enumerate(parts):
if i == len(parts) - 1:
# ensure final container exists and return it
if isinstance(p, int):
if not isinstance(cur, list):
raise TypeError("Final ensure segment expects list")
while len(cur) <= p:
cur.append(None)
if cur[p] is None:
cur[p] = factory()
return cur[p]
else:
if _is_mapping(cur):
if p not in cur or cur[p] is None:
cur[p] = factory()
return cur[p]
# object
existing = getattr(cur, p, None)
if existing is None:
existing = factory()
_set_attr(cur, p, existing, create_mapping=factory)
return existing
# intermediate
cur = _ensure_container(cur, p, create_mapping=factory)
return cur
# alias for symmetry
ensure = ensure_path