flattype 1.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flattype/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """FlatType for Python: JSON <-> Flat Notation, the compact ``key: value`` format of the @flattype/* npm packages.
2
+
3
+ >>> import flattype
4
+ >>> print(flattype.encode({"name": "John", "age": 30, "city": "New York"}))
5
+ name: John
6
+ age: 30
7
+ city: New York
8
+ >>> flattype.decode("name: John\\nage: 30")
9
+ {'name': 'John', 'age': 30}
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from dataclasses import dataclass
16
+ from typing import Any
17
+
18
+ from ._decode import Decoder, TableRow, decode
19
+ from ._encode import encode
20
+ from ._flatten import flatten, unflatten
21
+ from ._syntax import FlatDecodeError
22
+
23
+ __all__ = [
24
+ "MEDIA_TYPE",
25
+ "CONTENT_TYPE",
26
+ "Decoder",
27
+ "FlatDecodeError",
28
+ "FlatStats",
29
+ "TableRow",
30
+ "decode",
31
+ "dumps",
32
+ "encode",
33
+ "flatten",
34
+ "loads",
35
+ "stats",
36
+ "unflatten",
37
+ ]
38
+
39
+ __version__ = "1.11.0"
40
+
41
+ #: Media type for Flat Notation over HTTP.
42
+ MEDIA_TYPE = "text/x-flat"
43
+ #: ``Content-Type`` for Flat Notation responses.
44
+ CONTENT_TYPE = "text/x-flat; charset=utf-8"
45
+
46
+ #: ``json``-style aliases.
47
+ dumps = encode
48
+ loads = decode
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class FlatStats:
53
+ json_bytes: int
54
+ flat_bytes: int
55
+ #: Fraction saved, e.g. 0.45 for 45% smaller than minified JSON.
56
+ saved: float
57
+
58
+
59
+ def stats(value: Any, **options: Any) -> FlatStats:
60
+ """UTF-8 size of ``value`` as minified JSON and as Flat Notation."""
61
+ j = len(json.dumps(value, separators=(",", ":"), ensure_ascii=False).encode("utf-8", "surrogatepass"))
62
+ f = len(encode(value, **options).encode("utf-8", "surrogatepass"))
63
+ return FlatStats(j, f, 0.0 if j == 0 else 1 - f / j)
flattype/_decode.py ADDED
@@ -0,0 +1,294 @@
1
+ """Flat Notation -> Python values, whole or streamed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable
7
+
8
+ from ._path import Ref, Segment, assign, check, parse_path, resolve_refs, show_number
9
+ from ._syntax import INVALID, FlatDecodeError, format_key, js_trim, parse_scalar, scan_string
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class TableRow:
14
+ """One table row, passed to ``on_row`` as soon as its line has been read."""
15
+
16
+ #: Path of the table, e.g. ``"users"`` or ``"data.items"``; ``""`` for a root table.
17
+ path: str
18
+ #: Row number within the table, from 0.
19
+ index: int
20
+ value: Any
21
+
22
+
23
+ class _Table:
24
+ __slots__ = ("at", "segments", "count", "delimiter", "fields", "path", "rows", "read", "keys")
25
+
26
+ def __init__(self, at: int, segments: list[Segment], terminal: tuple[Any, ...]) -> None:
27
+ self.at = at
28
+ self.segments = segments
29
+ _, self.count, self.delimiter, self.fields = terminal
30
+ self.path = _format_path(segments)
31
+ self.rows: list[Any] = []
32
+ self.read = 0
33
+ # Fields that are plain keys or dot paths that can't conflict (the usual case) are built directly, without
34
+ # the general assign() and its duplicate / conflict checks.
35
+ paths = [tuple(f) for f in self.fields]
36
+ unique = set(paths)
37
+ simple = (
38
+ len(unique) == len(paths)
39
+ and all(isinstance(k, str) for p in paths for k in p)
40
+ and not any(p[:i] in unique for p in paths for i in range(1, len(p)))
41
+ )
42
+ self.keys: list[tuple[Any, ...]] | None = paths if simple else None
43
+
44
+
45
+ class Decoder:
46
+ """Incremental decoder: :meth:`write` chunks as they arrive (split anywhere, even mid-line), then :meth:`end`.
47
+
48
+ ``on_row`` is called for every table row as soon as its line is complete. With ``keep_rows=False``, rows are
49
+ only passed to ``on_row`` and their tables stay empty, so memory stays flat for huge lists.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ strict: bool = True,
56
+ refs: bool = False,
57
+ on_row: Callable[[TableRow], None] | None = None,
58
+ keep_rows: bool = True,
59
+ skip_invalid_lines: bool = False,
60
+ ) -> None:
61
+ self._strict = strict
62
+ # Lenient only: a line that can't be read is dropped, as if it weren't there.
63
+ self._skip = not strict and skip_invalid_lines
64
+ self._refs = refs
65
+ self._on_row = on_row
66
+ self._keep = keep_rows
67
+ self._holder: dict[str, Any] = {}
68
+ self._line_no = 0
69
+ # The unfinished last line, as a list of pieces: appending to one growing str would be quadratic.
70
+ self._rest: list[str] = []
71
+ self._table: _Table | None = None
72
+ self._pending: list[Ref] = []
73
+
74
+ def write(self, chunk: str) -> None:
75
+ """Feeds the next chunk of text."""
76
+ nl = chunk.find("\n")
77
+ if nl < 0:
78
+ if chunk:
79
+ self._rest.append(chunk)
80
+ return
81
+ self._rest.append(chunk[:nl])
82
+ self._line("".join(self._rest))
83
+ start = nl + 1
84
+ while True:
85
+ nl = chunk.find("\n", start)
86
+ if nl < 0:
87
+ break
88
+ self._line(chunk[start:nl])
89
+ start = nl + 1
90
+ self._rest = [chunk[start:]] if start < len(chunk) else []
91
+
92
+ def end(self) -> Any:
93
+ """Finishes decoding and returns the value. An empty document is ``{}``."""
94
+ self._line("".join(self._rest))
95
+ self._rest = []
96
+ table = self._table
97
+ if table is not None:
98
+ if self._strict:
99
+ raise FlatDecodeError(f"Expected {show_number(table.count)} rows, got {table.read}", table.at, 1)
100
+ try:
101
+ self._close_table()
102
+ except FlatDecodeError:
103
+ if not self._skip:
104
+ raise
105
+ if self._pending:
106
+ resolve_refs(self._holder, self._pending, self._strict)
107
+ return self._holder.get("root", {})
108
+
109
+ def _put(self, segments: list[Segment], value: Any, at: int) -> None:
110
+ """``assign`` into the result. When skipping lines, checked first, so a line that fails changes nothing."""
111
+ if self._skip:
112
+ check(self._holder, segments, self._strict, at)
113
+ assign(self._holder, segments, value, self._strict, at)
114
+
115
+ def _close_table(self) -> None:
116
+ # Cleared first: a table whose path can't be written is dropped, not left half-open.
117
+ t = self._table
118
+ assert t is not None
119
+ self._table = None
120
+ self._put(t.segments, t.rows, t.at)
121
+
122
+ def _line(self, raw: str) -> None:
123
+ self._line_no += 1
124
+ if not self._skip:
125
+ self._read_line(raw)
126
+ return
127
+ try:
128
+ self._read_line(raw)
129
+ except FlatDecodeError:
130
+ pass
131
+
132
+ def _read_line(self, raw: str) -> None:
133
+ line_no = self._line_no
134
+ strict = self._strict
135
+ text = raw[:-1] if raw.endswith("\r") else raw
136
+ t = self._table
137
+ if t is not None:
138
+ width = len(t.fields)
139
+ row = _cells(text, t.delimiter, strict, line_no, 1)
140
+ if len(row) != width and strict:
141
+ raise FlatDecodeError(f"Expected {width} values, got {len(row)}", line_no, 1)
142
+ if len(row) < width:
143
+ row += [None] * (width - len(row))
144
+ if t.keys is not None:
145
+ value: Any = {}
146
+ for path, cell in zip(t.keys, row):
147
+ d = value
148
+ for k in path[:-1]:
149
+ d = d.setdefault(k, {})
150
+ d[path[-1]] = cell
151
+ else:
152
+ obj: dict[str, Any] = {"root": {}}
153
+ for f in range(width):
154
+ assign(obj, t.fields[f], row[f], strict, line_no)
155
+ value = obj["root"]
156
+ if self._keep:
157
+ t.rows.append(value)
158
+ if self._on_row is not None:
159
+ self._on_row(TableRow(t.path, t.read, value))
160
+ t.read += 1
161
+ if t.read == t.count:
162
+ self._close_table()
163
+ return
164
+ pos = 0
165
+ n = len(text)
166
+ while pos < n and text[pos] in " \t":
167
+ pos += 1
168
+ if pos == n or text[pos] == "#":
169
+ return # blank or # comment
170
+
171
+ segments, terminal, end = parse_path(text, pos, line_no)
172
+ eq = end
173
+ while eq < n and text[eq] == " ":
174
+ eq += 1
175
+ if terminal[0] == "value" and eq < n and text[eq] == "=":
176
+ self._reference(segments, text, eq)
177
+ return
178
+ if end >= n or text[end] != ":":
179
+ raise FlatDecodeError("Expected ':' after path", line_no, end + 1)
180
+ value = text[end + 1 :]
181
+ kind = terminal[0]
182
+ if kind == "value":
183
+ self._put(segments, _scalar(js_trim(value), strict, line_no, end + 2), line_no)
184
+ elif kind == "inline":
185
+ self._put(segments, _cells(value, terminal[1], strict, line_no, end + 2), line_no)
186
+ else:
187
+ if js_trim(value) != "" and strict:
188
+ raise FlatDecodeError("Table rows go on the following lines", line_no, end + 2)
189
+ self._table = _Table(line_no, segments, terminal)
190
+ if terminal[1] == 0:
191
+ self._close_table()
192
+
193
+ def _reference(self, segments: list[Segment], text: str, eq: int) -> None:
194
+ """``path = target`` (``~`` is the root)."""
195
+ line_no = self._line_no
196
+ if not self._refs:
197
+ if self._strict:
198
+ raise FlatDecodeError("References need { refs: true }", line_no, eq + 1)
199
+ return
200
+ if not segments:
201
+ raise FlatDecodeError("The root can't be a reference", line_no, eq + 1)
202
+ pos = eq + 1
203
+ n = len(text)
204
+ while pos < n and text[pos] == " ":
205
+ pos += 1
206
+ if pos < n and text[pos] == "~":
207
+ target: list[Segment] = []
208
+ end = pos + 1
209
+ else:
210
+ target, terminal, end = parse_path(text, pos, line_no)
211
+ if terminal[0] != "value" or not target:
212
+ raise FlatDecodeError("Expected a path or '~' after '='", line_no, pos + 1)
213
+ while end < n and text[end] == " ":
214
+ end += 1
215
+ if end != n:
216
+ raise FlatDecodeError("Unexpected text after reference", line_no, end + 1)
217
+ ref = Ref(segments, target, line_no)
218
+ self._put(segments, ref, line_no)
219
+ self._pending.append(ref)
220
+
221
+
222
+ def decode(text: str, *, strict: bool = True, refs: bool = False, skip_invalid_lines: bool = False) -> Any:
223
+ """Decodes Flat Notation back into the value it came from. An empty document is ``{}``.
224
+
225
+ ``strict=False`` recovers what it can from malformed input (for example LLM output) instead of raising.
226
+ ``refs=True`` accepts ``path = other.path`` references and rebuilds the shared objects and cycles they describe.
227
+ It is off by default: a few such lines can describe a graph that is endless or enormous when walked.
228
+ ``skip_invalid_lines=True`` (with ``strict=False``) drops lines that can't be read, such as prose around data.
229
+ """
230
+ d = Decoder(strict=strict, refs=refs, skip_invalid_lines=skip_invalid_lines)
231
+ d.write(text)
232
+ return d.end()
233
+
234
+
235
+ def _format_path(segments: list[Segment]) -> str:
236
+ out = ""
237
+ for s in segments:
238
+ if not isinstance(s, str):
239
+ out += f"[{show_number(s)}]"
240
+ else:
241
+ out += format_key(s) if out == "" else "." + format_key(s)
242
+ return out
243
+
244
+
245
+ def _scalar(tok: str, strict: bool, line: int, column: int) -> Any:
246
+ if tok == "":
247
+ if strict:
248
+ raise FlatDecodeError("Missing value", line, column)
249
+ return None
250
+ v = parse_scalar(tok)
251
+ if v is INVALID:
252
+ if strict:
253
+ raise FlatDecodeError("Invalid quoted string", line, column)
254
+ return tok
255
+ return v
256
+
257
+
258
+ def _cells(s: str, delimiter: str, strict: bool, line: int, column: int) -> list[Any]:
259
+ """Splits ``a, "b, c", 3`` on the delimiter, honouring quotes."""
260
+ if '"' not in s:
261
+ # No quotes: a plain split gives the same cells. (Column numbers are only needed for errors.)
262
+ parts = [js_trim(x) for x in s.split(delimiter)]
263
+ if not strict or "" not in parts:
264
+ return [_scalar(p, strict, line, column) for p in parts]
265
+ out: list[Any] = []
266
+ n = len(s)
267
+ spaces = " " if delimiter == "\t" else " \t"
268
+ i = 0
269
+ while True:
270
+ while i < n and s[i] in spaces:
271
+ i += 1
272
+ if i < n and s[i] == '"':
273
+ close = scan_string(s, i)
274
+ if close < 0:
275
+ if strict:
276
+ raise FlatDecodeError("Unterminated string", line, column + i)
277
+ out.append(s[i + 1 :])
278
+ return out
279
+ out.append(_scalar(s[i : close + 1], strict, line, column + i))
280
+ i = close + 1
281
+ while i < n and s[i] in spaces:
282
+ i += 1
283
+ if i < n and s[i] != delimiter and strict:
284
+ raise FlatDecodeError("Expected delimiter after quoted string", line, column + i)
285
+ nxt = s.find(delimiter, i)
286
+ i = n if nxt < 0 else nxt
287
+ else:
288
+ nxt = s.find(delimiter, i)
289
+ j = n if nxt < 0 else nxt
290
+ out.append(_scalar(js_trim(s[i:j]), strict, line, column + i))
291
+ i = j
292
+ if i >= n:
293
+ return out
294
+ i += 1
flattype/_encode.py ADDED
@@ -0,0 +1,187 @@
1
+ """Python values -> Flat Notation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable
6
+
7
+ from ._syntax import format_number, format_primitive, join_key
8
+
9
+ _SCALARS = (str, int, float, bool, type(None))
10
+
11
+
12
+ def _key(k: Any) -> str:
13
+ """Dict keys as ``json.dumps`` converts them."""
14
+ if isinstance(k, str):
15
+ return k
16
+ if k is True:
17
+ return "true"
18
+ if k is False:
19
+ return "false"
20
+ if k is None:
21
+ return "null"
22
+ if isinstance(k, int):
23
+ return str(int(k))
24
+ if isinstance(k, float):
25
+ return format_number(k)
26
+ raise TypeError(f"keys must be str, int, float, bool or None, not {type(k).__name__}")
27
+
28
+
29
+ def encode(
30
+ value: Any,
31
+ *,
32
+ delimiter: str = ",",
33
+ indent: int = 2,
34
+ tabular: bool = True,
35
+ refs: bool = False,
36
+ default: Callable[[Any], Any] | None = None,
37
+ ) -> str:
38
+ """Encodes a JSON-compatible value (dicts, lists, tuples, str, int, float, bool, None) as Flat Notation::
39
+
40
+ name: John
41
+ address.city: New York
42
+ tags[]: admin, editor
43
+ users[2]{id,name}:
44
+ 1, Alice
45
+ 2, Bob
46
+
47
+ ``decode(encode(x)) == x`` for every such value. Floats are written like JavaScript writes them, so a float
48
+ with no fractional part (``30.0``) comes back as an int (``30``).
49
+
50
+ :param delimiter: separator for inline arrays and table rows: ``","``, ``"|"`` or ``"\\t"``.
51
+ :param indent: spaces before each table row.
52
+ :param tabular: write lists of same-shaped dicts as tables (header once, then one row each).
53
+ :param refs: write an object that appears more than once (shared, or a cycle) only once, and
54
+ ``path = first.path`` after that. Without it, shared objects are repeated and cycles raise ``ValueError``.
55
+ :param default: called for any other object, like ``json.dumps(default=...)``; returns something encodable.
56
+ """
57
+ if delimiter not in (",", "|", "\t"):
58
+ raise ValueError('delimiter must be ",", "|" or "\\t"')
59
+ pad = " " * indent
60
+ sep = ", " if delimiter == "," else " | " if delimiter == "|" else "\t"
61
+ mark = "" if delimiter == "," else delimiter
62
+ out: list[str] = []
63
+ # ids of the containers on the current path (cycle check)...
64
+ stack: set[int] = set()
65
+ # ...or with refs, every container and the path it was written at. The value is kept so its id stays unique.
66
+ seen: dict[int, tuple[str, Any]] | None = {} if refs else None
67
+
68
+ def prep(v: Any) -> Any:
69
+ if isinstance(v, (_SCALARS, dict, list, tuple)):
70
+ return v
71
+ if default is None:
72
+ raise TypeError(f"Object of type {type(v).__name__} is not Flat Notation serializable")
73
+ return prep(default(v))
74
+
75
+ def cell(v: Any) -> str:
76
+ return format_primitive(v, delimiter)
77
+
78
+ def visit(path: str, v: Any) -> None:
79
+ if not isinstance(v, (dict, list, tuple)):
80
+ out.append(path + ": " + format_primitive(v))
81
+ return
82
+ i = id(v)
83
+ if isinstance(v, tuple):
84
+ pass # Immutable, and Python shares equal tuple constants: never a reference, never a cycle on its own.
85
+ elif seen is not None:
86
+ first = seen.get(i)
87
+ if first is not None:
88
+ out.append(path + " = " + (first[0] or "~"))
89
+ return
90
+ seen[i] = (path, v)
91
+ else:
92
+ if i in stack:
93
+ raise ValueError("Circular reference detected (use refs=True)")
94
+ stack.add(i)
95
+ if isinstance(v, dict):
96
+ visit_dict(path, v)
97
+ else:
98
+ visit_list(path, v)
99
+ stack.discard(i)
100
+
101
+ def visit_dict(path: str, obj: dict[Any, Any]) -> None:
102
+ if not obj:
103
+ # An empty root dict is an empty document.
104
+ if path != "":
105
+ out.append(path + ": {}")
106
+ return
107
+ for k, child in obj.items():
108
+ visit(join_key(path, _key(k)), prep(child))
109
+
110
+ def visit_list(path: str, arr: Any) -> None:
111
+ if not arr:
112
+ out.append(path + ": []")
113
+ return
114
+ items = [prep(x) for x in arr]
115
+ if not any(isinstance(x, (dict, list, tuple)) for x in items):
116
+ out.append(path + "[" + mark + "]: " + sep.join(map(cell, items)))
117
+ return
118
+ if tabular and table(path, items):
119
+ return
120
+ for n, x in enumerate(items):
121
+ visit(path + "[" + str(n) + "]", x)
122
+
123
+ def taken(v: Any, path: str, claimed: dict[int, tuple[str, Any]]) -> bool:
124
+ """True if ``v`` can't go in a table row: it is on the current path (a cycle), or with refs, it was already
125
+ written or appears twice in this table. Otherwise, with refs, claims it for ``path``."""
126
+ i = id(v)
127
+ if i in stack:
128
+ return True
129
+ if seen is None:
130
+ return False
131
+ if i in claimed or i in seen:
132
+ return True
133
+ claimed[i] = (path, v)
134
+ return False
135
+
136
+ def flatten_row(
137
+ obj: dict[Any, Any], prefix: str, at: str, keys: list[str], vals: list[Any], claimed: dict[int, tuple[str, Any]]
138
+ ) -> bool:
139
+ """Collects the leaf paths and values of one row. False if the row can't be a table row."""
140
+ if not obj:
141
+ return False
142
+ for k, child in obj.items():
143
+ v = prep(child)
144
+ p = join_key(prefix, _key(k))
145
+ if isinstance(v, (dict, list, tuple)):
146
+ if not isinstance(v, dict) or taken(v, at + "." + p, claimed):
147
+ return False
148
+ stack.add(id(v))
149
+ ok = flatten_row(v, p, at, keys, vals, claimed)
150
+ stack.discard(id(v))
151
+ if not ok:
152
+ return False
153
+ else:
154
+ keys.append(p)
155
+ vals.append(v)
156
+ return True
157
+
158
+ def table(path: str, items: list[Any]) -> bool:
159
+ """Writes same-shaped dicts as ``path[n]{a,b.c}:`` plus one row each."""
160
+ header: list[str] | None = None
161
+ rows: list[str] = []
162
+ claimed: dict[int, tuple[str, Any]] = {}
163
+ for n, item in enumerate(items):
164
+ at = path + "[" + str(n) + "]"
165
+ if not isinstance(item, dict) or taken(item, at, claimed):
166
+ return False
167
+ keys: list[str] = []
168
+ vals: list[Any] = []
169
+ stack.add(id(item))
170
+ ok = flatten_row(item, "", at, keys, vals, claimed)
171
+ stack.discard(id(item))
172
+ if not ok:
173
+ return False
174
+ if header is None:
175
+ header = keys
176
+ elif keys != header:
177
+ return False
178
+ rows.append(pad + sep.join(map(cell, vals)))
179
+ if seen is not None:
180
+ seen.update(claimed)
181
+ assert header is not None
182
+ out.append(path + "[" + str(len(items)) + mark + "]{" + delimiter.join(header) + "}:")
183
+ out.extend(rows)
184
+ return True
185
+
186
+ visit("", prep(value))
187
+ return "\n".join(out)
flattype/_flatten.py ADDED
@@ -0,0 +1,55 @@
1
+ """Path -> leaf dicts for in-memory use: ``{"user.name": "John", "tags[0]": "a"}``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ from ._path import assign, parse_path
9
+ from ._syntax import FlatDecodeError, join_key
10
+
11
+
12
+ def flatten(value: Any) -> dict[str, Any]:
13
+ """Flattens a value into a path -> leaf dict, e.g. ``{"user": {"name": "John"}}`` -> ``{"user.name": "John"}``.
14
+
15
+ Empty dicts and lists are kept as ``{}`` / ``[]`` leaves so :func:`unflatten` can restore them. A scalar root is
16
+ stored under the empty path ``""``.
17
+ """
18
+ out: dict[str, Any] = {}
19
+ stack: set[int] = set()
20
+
21
+ def visit(path: str, v: Any) -> None:
22
+ if not isinstance(v, (dict, list, tuple)):
23
+ if not isinstance(v, (str, int, float, type(None))):
24
+ raise TypeError(f"Object of type {type(v).__name__} is not Flat Notation serializable")
25
+ out[path] = None if isinstance(v, float) and not math.isfinite(v) else v
26
+ return
27
+ if id(v) in stack:
28
+ raise ValueError("Circular reference detected")
29
+ stack.add(id(v))
30
+ if isinstance(v, dict):
31
+ if not v and path != "":
32
+ out[path] = {}
33
+ for k, x in v.items():
34
+ visit(join_key(path, str(k)), x)
35
+ else:
36
+ if not v:
37
+ out[path] = []
38
+ for i, x in enumerate(v):
39
+ visit(f"{path}[{i}]", x)
40
+ stack.discard(id(v))
41
+
42
+ visit("", value)
43
+ return out
44
+
45
+
46
+ def unflatten(record: dict[str, Any]) -> Any:
47
+ """Rebuilds the value :func:`flatten` produced. Raises :class:`FlatDecodeError` on a malformed or conflicting path."""
48
+ holder: dict[str, Any] = {}
49
+ for n, path in enumerate(record, 1):
50
+ segments, terminal, end = parse_path(path, 0, n)
51
+ if terminal[0] != "value" or end != len(path):
52
+ raise FlatDecodeError(f"Invalid path '{path}'", n, end + 1)
53
+ leaf = record[path]
54
+ assign(holder, segments, [] if isinstance(leaf, list) else {} if isinstance(leaf, dict) else leaf, True, n)
55
+ return holder.get("root", {})