gcf-python 2.1.0__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
@@ -151,6 +151,17 @@ def _parse_object_body(
151
151
  i += consumed
152
152
  continue
153
153
 
154
+ # Key=value. Check before inline array so bracket patterns in quoted
155
+ # values (e.g. text="ERR[404]: Not Found") are not misinterpreted.
156
+ eq_idx = _find_kv_split(content)
157
+ if eq_idx > 0:
158
+ name = _parse_key_from_header(content[:eq_idx])
159
+ _check_dup(out, name)
160
+ out[name] = parse_scalar(content[eq_idx + 1:])
161
+ i += 1
162
+ continue
163
+
164
+ # Inline array (e.g. items[3]: a,b,c). Only reached if no = found.
154
165
  if not content.startswith("@") and not content.startswith("##"):
155
166
  bracket_idx = content.find("[")
156
167
  if bracket_idx > 0:
@@ -166,14 +177,6 @@ def _parse_object_body(
166
177
  i += 1
167
178
  continue
168
179
 
169
- eq_idx = _find_kv_split(content)
170
- if eq_idx > 0:
171
- name = _parse_key_from_header(content[:eq_idx])
172
- _check_dup(out, name)
173
- out[name] = parse_scalar(content[eq_idx + 1:])
174
- i += 1
175
- continue
176
-
177
180
  i += 1
178
181
  return i - start
179
182
 
@@ -191,7 +194,13 @@ def _find_kv_split(s: str) -> int:
191
194
  return i + 1 if i + 1 < len(s) and s[i + 1] == "=" else -1
192
195
  i += 1
193
196
  return -1
194
- return s.find("=")
197
+ eq_idx = s.find("=")
198
+ if eq_idx < 0:
199
+ return -1
200
+ bracket_idx = s.find("[")
201
+ if bracket_idx >= 0 and bracket_idx < eq_idx:
202
+ return -1
203
+ return eq_idx
195
204
 
196
205
 
197
206
  def _parse_key_from_header(s: str) -> str:
@@ -354,6 +363,55 @@ def _parse_attachment(
354
363
  raise ValueError(f"invalid attachment form: {after_name}")
355
364
 
356
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
+
357
415
  def _parse_tabular_body(
358
416
  lines: list[str], start: int, depth: int, fields: list[str], expected_count: int
359
417
  ) -> tuple[list[Any], int]:
@@ -361,6 +419,12 @@ def _parse_tabular_body(
361
419
  rows: list[Any] = []
362
420
  i = start
363
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
+
364
428
  # Track inline schemas and shared array schemas.
365
429
  inline_schemas: dict[str, list[str]] = {}
366
430
  shared_array_schemas: dict[str, list[str]] = {}
@@ -400,9 +464,22 @@ def _parse_tabular_body(
400
464
  inline_att_order: list[str] = []
401
465
  missing_fields: set[str] = set()
402
466
 
467
+ # Collect path column values for unflattening.
468
+ flat_values: dict[str, Any] = {}
469
+ flat_absent: set[str] = set()
470
+
403
471
  for j, f in enumerate(fields):
404
472
  cell_val = vals[j]
405
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
+
406
483
  # Check for ^{fields} inline schema declaration.
407
484
  if cell_val.startswith("^{") and cell_val.endswith("}"):
408
485
  schema_str = cell_val[1:]
@@ -539,6 +616,11 @@ def _parse_tabular_body(
539
616
  row[f] = cell_values[f]
540
617
  elif f in attachment_values:
541
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
+
542
624
  rows.append(row)
543
625
 
544
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
gcf/scalar.py CHANGED
@@ -8,6 +8,7 @@ from typing import Any
8
8
 
9
9
  _JSON_NUMBER_RE = re.compile(r"^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$")
10
10
  _NUMERIC_LIKE_RE = re.compile(r"^[+-]\.?\d|^\.\d|^0\d")
11
+ _INLINE_ARRAY_RE = re.compile(r"\[[^\]]*\]\s*:")
11
12
  _BARE_KEY_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
12
13
 
13
14
 
@@ -36,6 +37,8 @@ def needs_quote(s: str) -> bool:
36
37
  return True
37
38
  if s[0] in ("#", "@", "."):
38
39
  return True
40
+ if _INLINE_ARRAY_RE.search(s):
41
+ return True
39
42
  for c in s:
40
43
  o = ord(c)
41
44
  if c in ('"', "\\", "|", ",") or o < 0x20 or c in ("\n", "\r"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gcf-python
3
- Version: 2.1.0
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/
@@ -217,26 +217,26 @@ Works on dicts, lists, and primitives. Lists of uniform dicts get tabular rows.
217
217
 
218
218
  GCF wins 13/15 datasets on the expanded [token efficiency benchmark](https://github.com/blackwell-systems/toon/tree/gcf-comparison). Full results: [gcformat.com/guide/benchmarks](https://gcformat.com/guide/benchmarks.html)
219
219
 
220
- ## Links
221
-
222
- - [Documentation](https://gcformat.com/)
223
- - [Playground](https://gcformat.com/playground.html)
224
- - [Specification](https://github.com/blackwell-systems/gcf)
225
- - [Go library](https://github.com/blackwell-systems/gcf-go)
226
- - [TypeScript library](https://github.com/blackwell-systems/gcf-typescript)
227
- - [MCP Proxy](https://github.com/blackwell-systems/gcf-proxy) (zero-code adoption)
228
- - [GCF vs TOON](https://gcformat.com/guide/vs-toon.html)
229
- - [TOON benchmark fork](https://github.com/blackwell-systems/toon/tree/gcf-comparison)
230
-
231
-
232
- <details>
233
- <summary>More links</summary>
234
-
235
- - [betterthanjson.com](https://betterthanjson.com)
236
- - [jsonalternative.com](https://jsonalternative.com)
237
- - [betterthantoon.com](https://betterthantoon.com)
238
-
239
- </details>
220
+ ## Implementations
221
+
222
+ | Language | Package | Repository |
223
+ |----------|---------|-----------|
224
+ | Go | `go get github.com/blackwell-systems/gcf-go` | [gcf-go](https://github.com/blackwell-systems/gcf-go) |
225
+ | TypeScript | `npm install @blackwell-systems/gcf` | [gcf-typescript](https://github.com/blackwell-systems/gcf-typescript) |
226
+ | Python | `pip install gcf-python` | [gcf-python](https://github.com/blackwell-systems/gcf-python) |
227
+ | Rust | `cargo add gcf` | [gcf-rust](https://github.com/blackwell-systems/gcf-rust) |
228
+ | Swift | Swift Package Manager | [gcf-swift](https://github.com/blackwell-systems/gcf-swift) |
229
+ | Kotlin | JitPack | [gcf-kotlin](https://github.com/blackwell-systems/gcf-kotlin) |
230
+ | MCP Proxy | `pip install gcf-proxy` | [gcf-proxy](https://github.com/blackwell-systems/gcf-proxy) (bidirectional, session dedup, HTTP frontend) |
231
+ | Claude Code Plugin | `/plugin install` | [gcf-claude-plugin](https://github.com/blackwell-systems/gcf-claude-plugin) (one-command install, session stats hook) |
232
+ | Codex Plugin | `codex plugin add` | [gcf-codex-plugin](https://github.com/blackwell-systems/gcf-codex-plugin) (one-command install, session stats hook) |
233
+ | VS Code | `ext install blackwell-systems.gcf-vscode` | [gcf-vscode](https://marketplace.visualstudio.com/items?itemName=blackwell-systems.gcf-vscode) (syntax highlighting) |
234
+ | n8n | `npm install n8n-nodes-gcf` | [gcf-n8n-nodes](https://github.com/blackwell-systems/gcf-n8n-nodes) (workflow encode/decode) |
235
+ | Tree-sitter | `npm install tree-sitter-gcf` | [tree-sitter-gcf](https://github.com/blackwell-systems/tree-sitter-gcf) |
236
+
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
+
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
 
241
241
  ## License
242
242
 
@@ -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=49_X9fKuBctd9Djd1103iaZWpPZX8TP_KgnWpji130s,23428
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
10
- gcf/scalar.py,sha256=YBPTGcgFPwN_lgPLuRg8U4uHVlJ2vn5d1Gz1AGf-E00,9225
9
+ gcf/generic.py,sha256=7cSwYT_U3ySTZy8yLY_GtQrzzGIRkhHOE1SEIGmhdrg,15458
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.0.dist-info/METADATA,sha256=f4x6jJYGrKcClX0G9OeJDPUgRv6SGCOQlUmQivk45MA,8234
16
- gcf_python-2.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
- gcf_python-2.1.0.dist-info/entry_points.txt,sha256=aFT6gqlkh8iGfM8cblE-LUMxHH08_v71IIoZtDdRIVA,37
18
- gcf_python-2.1.0.dist-info/licenses/LICENSE,sha256=2Fit9wnaIe--RMSAgyQqxC5hfZTyZqn4fIdBtp9qPDw,1072
19
- gcf_python-2.1.0.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,,