gcf-python 2.1.1__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {gcf_python-2.1.1 → gcf_python-2.2.0}/CHANGELOG.md +11 -0
  2. {gcf_python-2.1.1 → gcf_python-2.2.0}/PKG-INFO +2 -2
  3. {gcf_python-2.1.1 → gcf_python-2.2.0}/README.md +1 -1
  4. {gcf_python-2.1.1 → gcf_python-2.2.0}/pyproject.toml +1 -1
  5. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/decode_generic.py +73 -0
  6. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/generic.py +145 -5
  7. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_generic.py +5 -9
  8. {gcf_python-2.1.1 → gcf_python-2.2.0}/.github/FUNDING.yml +0 -0
  9. {gcf_python-2.1.1 → gcf_python-2.2.0}/.github/workflows/ci.yml +0 -0
  10. {gcf_python-2.1.1 → gcf_python-2.2.0}/.github/workflows/publish.yml +0 -0
  11. {gcf_python-2.1.1 → gcf_python-2.2.0}/.gitignore +0 -0
  12. {gcf_python-2.1.1 → gcf_python-2.2.0}/LICENSE +0 -0
  13. {gcf_python-2.1.1 → gcf_python-2.2.0}/assets/gcf-python-diagram.png +0 -0
  14. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/__init__.py +0 -0
  15. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/__main__.py +0 -0
  16. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/cli.py +0 -0
  17. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/constants.py +0 -0
  18. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/decode.py +0 -0
  19. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/delta.py +0 -0
  20. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/encode.py +0 -0
  21. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/scalar.py +0 -0
  22. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/session.py +0 -0
  23. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/stream.py +0 -0
  24. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/stream_generic.py +0 -0
  25. {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/types.py +0 -0
  26. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/__init__.py +0 -0
  27. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_conformance_v2.py +0 -0
  28. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_decode.py +0 -0
  29. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_delta.py +0 -0
  30. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_encode.py +0 -0
  31. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_roundtrip.py +0 -0
  32. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_roundtrip_v2.py +0 -0
  33. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_session.py +0 -0
  34. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_stream.py +0 -0
  35. {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_stream_generic.py +0 -0
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.2.0 (2026-06-22)
4
+
5
+ ### Spec v3.2: Nested Object Flattening
6
+
7
+ - Encoder automatically flattens fixed-shape nested objects into `>` path column names (e.g., `"customer>name"` instead of `^` + `.customer {}` attachment)
8
+ - Decoder reconstructs nested objects from `>` path columns
9
+ - 20-48% fewer tokens on deeply nested API data (Jira, Stripe, K8s, calendar events)
10
+ - 100% comprehension on every frontier model (validated across 9 models, 7 providers)
11
+ - Zero regression on lossless round-trips (230 tests, conformance + property-based)
12
+ - Falls back to attachment mechanism for: variable-length arrays, objects with different keys across rows, objects with `>` in key names, empty nested objects
13
+
3
14
  ## v2.1.0 (2026-06-14)
4
15
 
5
16
  ### Spec v3.1
@@ -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
 
@@ -209,7 +209,7 @@ GCF wins 13/15 datasets on the expanded [token efficiency benchmark](https://git
209
209
  | n8n | `npm install n8n-nodes-gcf` | [gcf-n8n-nodes](https://github.com/blackwell-systems/gcf-n8n-nodes) (workflow encode/decode) |
210
210
  | Tree-sitter | `npm install tree-sitter-gcf` | [tree-sitter-gcf](https://github.com/blackwell-systems/tree-sitter-gcf) |
211
211
 
212
- Zero runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
212
+ **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.
213
213
 
214
214
  **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.
215
215
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gcf-python"
7
- version = "2.1.1"
7
+ version = "2.2.0"
8
8
  description = "Drop-in JSON replacement for AI pipelines. 79% fewer tokens. 90.7% comprehension across 10 models. Zero dependencies."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -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:
@@ -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
@@ -60,15 +60,11 @@ def test_encode_mixed_data():
60
60
  }
61
61
  output = encode_generic(data)
62
62
 
63
- # Header includes all fields (v2.0: nested fields in union too).
64
- assert "## projects [2]{name,status,config}" in output
65
- # Rows with nested data get @id prefix and ^ marker.
66
- assert "@0 Alpha|active|^" in output
67
- assert "@1 Beta|draft|^" in output
68
- # Nested config uses .field {} attachment.
69
- assert ".config {}" in output
70
- assert "env=" in output
71
- assert "region=" in output
63
+ # Header includes flattened path columns (v3.2).
64
+ assert '## projects [2]{name,status,"config>env","config>region"}' in output
65
+ # Rows with flattened data are pure tabular.
66
+ assert "Alpha|active|prod|us-east" in output
67
+ assert "Beta|draft|staging|eu-west" in output
72
68
 
73
69
 
74
70
  def test_encode_none_value():
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes