gcf-python 2.1.1__py3-none-any.whl → 2.2.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.
gcf/decode_generic.py CHANGED
@@ -363,6 +363,55 @@ def _parse_attachment(
363
363
  raise ValueError(f"invalid attachment form: {after_name}")
364
364
 
365
365
 
366
+ def _unflatten_paths(
367
+ path_columns: dict[str, list[str]],
368
+ flat_values: dict[str, Any],
369
+ flat_absent: set[str],
370
+ ) -> dict[str, Any]:
371
+ """Reconstruct nested objects from flat > path columns."""
372
+ groups: dict[str, list[str]] = {}
373
+ group_order: list[str] = []
374
+ for field_name, paths in path_columns.items():
375
+ if not paths:
376
+ continue
377
+ top = paths[0]
378
+ if top not in groups:
379
+ groups[top] = []
380
+ group_order.append(top)
381
+ groups[top].append(field_name)
382
+
383
+ result: dict[str, Any] = {}
384
+
385
+ for top in group_order:
386
+ field_names = groups[top]
387
+ all_absent = all(f in flat_absent for f in field_names)
388
+ all_null = all(
389
+ (f not in flat_absent and flat_values.get(f) is None)
390
+ for f in field_names
391
+ )
392
+
393
+ if all_absent:
394
+ continue
395
+ if all_null:
396
+ result[top] = None
397
+ continue
398
+
399
+ for field_name in field_names:
400
+ if field_name in flat_absent:
401
+ continue
402
+ paths = path_columns[field_name]
403
+ val = flat_values.get(field_name)
404
+
405
+ current = result
406
+ for k in paths[:-1]:
407
+ if k not in current:
408
+ current[k] = {}
409
+ current = current[k]
410
+ current[paths[-1]] = val
411
+
412
+ return result
413
+
414
+
366
415
  def _parse_tabular_body(
367
416
  lines: list[str], start: int, depth: int, fields: list[str], expected_count: int
368
417
  ) -> tuple[list[Any], int]:
@@ -370,6 +419,12 @@ def _parse_tabular_body(
370
419
  rows: list[Any] = []
371
420
  i = start
372
421
 
422
+ # Detect path columns: fields containing ">".
423
+ path_column_map: dict[str, list[str]] = {}
424
+ for f in fields:
425
+ if ">" in f:
426
+ path_column_map[f] = f.split(">")
427
+
373
428
  # Track inline schemas and shared array schemas.
374
429
  inline_schemas: dict[str, list[str]] = {}
375
430
  shared_array_schemas: dict[str, list[str]] = {}
@@ -409,9 +464,22 @@ def _parse_tabular_body(
409
464
  inline_att_order: list[str] = []
410
465
  missing_fields: set[str] = set()
411
466
 
467
+ # Collect path column values for unflattening.
468
+ flat_values: dict[str, Any] = {}
469
+ flat_absent: set[str] = set()
470
+
412
471
  for j, f in enumerate(fields):
413
472
  cell_val = vals[j]
414
473
 
474
+ # Path columns: store values for later unflattening.
475
+ if f in path_column_map:
476
+ parsed = parse_scalar(cell_val, tabular_context=True)
477
+ if parsed is MISSING:
478
+ flat_absent.add(f)
479
+ else:
480
+ flat_values[f] = parsed
481
+ continue
482
+
415
483
  # Check for ^{fields} inline schema declaration.
416
484
  if cell_val.startswith("^{") and cell_val.endswith("}"):
417
485
  schema_str = cell_val[1:]
@@ -548,6 +616,11 @@ def _parse_tabular_body(
548
616
  row[f] = cell_values[f]
549
617
  elif f in attachment_values:
550
618
  row[f] = attachment_values[f]
619
+ # Unflatten path columns into nested objects.
620
+ if path_column_map:
621
+ nested = _unflatten_paths(path_column_map, flat_values, flat_absent)
622
+ row.update(nested)
623
+
551
624
  rows.append(row)
552
625
 
553
626
  if expected_count >= 0 and len(rows) >= expected_count:
gcf/generic.py CHANGED
@@ -145,15 +145,136 @@ def _shared_array_schema(arr: list[dict], field_name: str) -> list[str] | None:
145
145
  return canonical_fields
146
146
 
147
147
 
148
+ # ── Nested object flattening (v3.2) ──────────────────────────────────────
149
+
150
+
151
+ def _analyze_flattenable(
152
+ arr: list[dict], field_name: str, parent_path: str
153
+ ) -> list[dict] | None:
154
+ """Analyze whether a field can be flattened. Returns list of leaf descriptors or None."""
155
+ canonical_shape: dict[str, str] | None = None # key -> "scalar" | "nested"
156
+
157
+ for item in arr:
158
+ if field_name not in item or item[field_name] is None:
159
+ continue
160
+ v = item[field_name]
161
+ if not isinstance(v, dict):
162
+ return None
163
+ if isinstance(v, list):
164
+ return None
165
+
166
+ keys = list(v.keys())
167
+
168
+ if canonical_shape is None:
169
+ canonical_shape = {}
170
+ for k in keys:
171
+ if ">" in k:
172
+ return None
173
+ val = v[k]
174
+ if isinstance(val, list):
175
+ return None
176
+ elif isinstance(val, dict):
177
+ canonical_shape[k] = "nested"
178
+ else:
179
+ canonical_shape[k] = "scalar"
180
+ else:
181
+ if len(keys) != len(canonical_shape):
182
+ return None
183
+ for k in keys:
184
+ if k not in canonical_shape:
185
+ return None
186
+ val = v[k]
187
+ expected = canonical_shape[k]
188
+ if expected == "scalar":
189
+ if isinstance(val, (dict, list)):
190
+ return None
191
+ elif expected == "nested":
192
+ if isinstance(val, list):
193
+ return None
194
+ if val is not None and not isinstance(val, dict):
195
+ return None
196
+
197
+ if canonical_shape is None:
198
+ return None
199
+
200
+ current_path = f"{parent_path}>{field_name}" if parent_path else field_name
201
+ parent_keys = parent_path.split(">") + [field_name] if parent_path else [field_name]
202
+
203
+ leaves: list[dict] = []
204
+ for k in canonical_shape:
205
+ if canonical_shape[k] == "scalar":
206
+ leaves.append({"path": f"{current_path}>{k}", "keys": parent_keys + [k]})
207
+ else:
208
+ sub_arr = []
209
+ for item in arr:
210
+ if field_name not in item or item[field_name] is None:
211
+ sub_arr.append({})
212
+ else:
213
+ sub_arr.append(item[field_name])
214
+ sub_leaves = _analyze_flattenable(sub_arr, k, current_path)
215
+ if sub_leaves is None or len(sub_leaves) == 0:
216
+ return None
217
+ leaves.extend(sub_leaves)
218
+
219
+ # Guard: reject if any row has non-null object with all-null leaves.
220
+ if leaves:
221
+ for item in arr:
222
+ if field_name not in item or item[field_name] is None:
223
+ continue
224
+ all_null = all(
225
+ _resolve_key_chain(item, leaf["keys"])[0] is None
226
+ and _resolve_key_chain(item, leaf["keys"])[1]
227
+ for leaf in leaves
228
+ )
229
+ if all_null:
230
+ return None
231
+
232
+ return leaves
233
+
234
+
235
+ def _resolve_key_chain(item: Any, keys: list[str]) -> tuple[Any, bool]:
236
+ """Traverse an object by key chain. Returns (value, exists)."""
237
+ if not keys or not isinstance(item, dict):
238
+ return None, False
239
+ if keys[0] not in item:
240
+ return None, False
241
+ current = item[keys[0]]
242
+ if current is None:
243
+ return None, True
244
+ for k in keys[1:]:
245
+ if not isinstance(current, dict) or k not in current:
246
+ return None, False
247
+ current = current[k]
248
+ return current, True
249
+
250
+
148
251
  def _encode_tabular(
149
252
  header_prefix: str, arr: list[dict], fields: list[str], out: list[str], depth: int
150
253
  ) -> None:
151
254
  prefix = _indent(depth)
152
255
 
153
- # Pre-compute inline schemas and shared array schemas.
256
+ # Phase 0: Analyze fields for flattening.
257
+ flatten_map: dict[str, list[dict]] = {}
258
+ for f in fields:
259
+ leaves = _analyze_flattenable(arr, f, "")
260
+ if leaves and len(leaves) > 0:
261
+ flatten_map[f] = leaves
262
+
263
+ # Build expanded column list.
264
+ columns: list[dict] = []
265
+ for f in fields:
266
+ if f in flatten_map:
267
+ for leaf in flatten_map[f]:
268
+ columns.append({"header": format_key(leaf["path"]), "type": "flat", "field": f, "keys": leaf["keys"]})
269
+ else:
270
+ columns.append({"header": format_key(f), "type": "original", "field": f, "keys": []})
271
+
272
+ # Pre-compute inline schemas and shared array schemas (skip flattened fields).
154
273
  inline_schemas: dict[str, list[str]] = {}
155
274
  shared_arr_schemas: dict[str, list[str]] = {}
156
275
  for f in fields:
276
+ if f in flatten_map:
277
+ continue
157
278
  ifs = _inline_schema_fields(arr, f)
158
279
  if ifs is not None:
159
280
  inline_schemas[f] = ifs
@@ -161,15 +282,34 @@ def _encode_tabular(
161
282
  if sas is not None:
162
283
  shared_arr_schemas[f] = sas
163
284
 
164
- fmt_fields = ",".join(format_key(f) for f in fields)
165
- out.append(f"{header_prefix}[{len(arr)}]{{{fmt_fields}}}")
285
+ header_fields = ",".join(col["header"] for col in columns)
286
+ out.append(f"{header_prefix}[{len(arr)}]{{{header_fields}}}")
166
287
 
167
288
  for i, item in enumerate(arr):
168
289
  cells: list[str] = []
169
- attachments: list[tuple[str, Any, bool, list[str] | None]] = [] # (name, value, inline, inline_fields)
290
+ attachments: list[tuple[str, Any, bool, list[str] | None]] = []
170
291
  row_has_attachment = False
171
292
 
172
- for f in fields:
293
+ for col in columns:
294
+ if col["type"] == "flat":
295
+ keys = col["keys"]
296
+ if keys[0] not in item:
297
+ cells.append("~")
298
+ else:
299
+ top_val = item[keys[0]]
300
+ if top_val is None:
301
+ cells.append("-")
302
+ else:
303
+ val, exists = _resolve_key_chain(item, keys)
304
+ if not exists:
305
+ cells.append("~")
306
+ elif val is None:
307
+ cells.append("-")
308
+ else:
309
+ cells.append(format_scalar(val, "|"))
310
+ continue
311
+
312
+ f = col["field"]
173
313
  if f not in item:
174
314
  cells.append("~")
175
315
  continue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gcf-python
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: Drop-in JSON replacement for AI pipelines. 79% fewer tokens. 90.7% comprehension across 10 models. Zero dependencies.
5
5
  Project-URL: Homepage, https://github.com/blackwell-systems/gcf-python
6
6
  Project-URL: Documentation, https://blackwell-systems.github.io/gcf/
@@ -234,7 +234,7 @@ GCF wins 13/15 datasets on the expanded [token efficiency benchmark](https://git
234
234
  | n8n | `npm install n8n-nodes-gcf` | [gcf-n8n-nodes](https://github.com/blackwell-systems/gcf-n8n-nodes) (workflow encode/decode) |
235
235
  | Tree-sitter | `npm install tree-sitter-gcf` | [tree-sitter-gcf](https://github.com/blackwell-systems/tree-sitter-gcf) |
236
236
 
237
- Zero runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
237
+ **Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
238
238
 
239
239
  **Specification:** [SPEC v3.1 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 157 conformance fixtures, 33,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. All implementations at v2.1.0+ (Go v1.2.0). Cross-language 6x6 matrix verified.
240
240
 
@@ -3,17 +3,17 @@ gcf/__main__.py,sha256=EpvBz1yc8H0D5OJ1zy2tYke-kRzvudKa4DEbfeW14ao,71
3
3
  gcf/cli.py,sha256=UEe1CAZn-rKGNIo_ap8-oez3ucl6DSRbsdv6RDnzygY,5256
4
4
  gcf/constants.py,sha256=cmZ8YJSOB0im_eyfN8v4UvrLpBC6Fuf4cfcKZGbutxY,638
5
5
  gcf/decode.py,sha256=nD8bXYhoeHQQ3LCeAJQOAgFuob-V_6us4mcBYtL_bBc,5978
6
- gcf/decode_generic.py,sha256=fb0vh14zbyS0qUMN-hEKouD7Nr99Ntw5eiSTgbGfUV0,23810
6
+ gcf/decode_generic.py,sha256=QdwXIPZhTTJsfaYInYKW0TKr8Ln5cufmRE2kl40_kpY,26034
7
7
  gcf/delta.py,sha256=f90UC6zejXH-ujyU_OViWDCqnxLXk3i6Qion-RJGHY4,1670
8
8
  gcf/encode.py,sha256=CcqMSNmojrQAAw2X3MNfw8YR6l4rNw9hWDqwmf846oA,2929
9
- gcf/generic.py,sha256=h9IcvX0MCSG0kbbbGqlBQKe7uaPrBM9ewjJzGpgUn88,10432
9
+ gcf/generic.py,sha256=7cSwYT_U3ySTZy8yLY_GtQrzzGIRkhHOE1SEIGmhdrg,15458
10
10
  gcf/scalar.py,sha256=MZay-KIROvaFHet-g2-pBghahT1bf_5bxZjG4yTZkSo,9329
11
11
  gcf/session.py,sha256=6-wytNGaBkgqH7uTar7ojUFYNwIMeEVAZhuV-s-upZM,4704
12
12
  gcf/stream.py,sha256=1Kt_a2daKpYHlWP1lnDJ4g549pUt9rc-U0xbARbXRKQ,5174
13
13
  gcf/stream_generic.py,sha256=RnqqiPSu5joJa-7e58QbbzSGfvxBICA587A3aArjZvE,3250
14
14
  gcf/types.py,sha256=AWm-LQoSqLHAYtEjcAxWQZqJ4JXqNreLUKO2mJFgNMA,1465
15
- gcf_python-2.1.1.dist-info/METADATA,sha256=LNaTf48XfyKNcSvZ0gI8zNMCWKwUgbB8hiTugMfs45I,9554
16
- gcf_python-2.1.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
- gcf_python-2.1.1.dist-info/entry_points.txt,sha256=aFT6gqlkh8iGfM8cblE-LUMxHH08_v71IIoZtDdRIVA,37
18
- gcf_python-2.1.1.dist-info/licenses/LICENSE,sha256=2Fit9wnaIe--RMSAgyQqxC5hfZTyZqn4fIdBtp9qPDw,1072
19
- gcf_python-2.1.1.dist-info/RECORD,,
15
+ gcf_python-2.2.0.dist-info/METADATA,sha256=Kc-fEX3jOtbvgBPwEiQL0gRf4hibJYlGJu4ea05kGfw,9781
16
+ gcf_python-2.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
+ gcf_python-2.2.0.dist-info/entry_points.txt,sha256=aFT6gqlkh8iGfM8cblE-LUMxHH08_v71IIoZtDdRIVA,37
18
+ gcf_python-2.2.0.dist-info/licenses/LICENSE,sha256=2Fit9wnaIe--RMSAgyQqxC5hfZTyZqn4fIdBtp9qPDw,1072
19
+ gcf_python-2.2.0.dist-info/RECORD,,