gcf-python 2.7.1__py3-none-any.whl → 2.8.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/__init__.py CHANGED
@@ -39,6 +39,7 @@ from .decode import DecodeError, decode
39
39
  from .delta import decode_delta, encode_delta, verify_delta
40
40
  from .encode import encode
41
41
  from .generic import encode_generic, GenericOptions
42
+ from .constant_grouping import encode_generic_grouped
42
43
  from .generic_delta import (
43
44
  GenericSet,
44
45
  GenericDeltaPayload,
@@ -82,6 +83,7 @@ __all__ = [
82
83
  "encode_delta",
83
84
  "verify_delta",
84
85
  "encode_generic",
86
+ "encode_generic_grouped",
85
87
  "GenericOptions",
86
88
  "encode_with_session",
87
89
  "GenericSet",
@@ -0,0 +1,549 @@
1
+ """GCF v3.6.0 tabular column optimizations for the generic profile: constant-column
2
+ factoring (SPEC 7.4.7) and value-grouping (SPEC 7.4.8).
3
+
4
+ Constant-column factoring is mandatory canonical and lives in the encoder
5
+ (_encode_tabular, generic.py); the decode side and the opt-in grouped encoder are
6
+ here. The wire output of encode_generic_grouped is byte-identical to the Go reference
7
+ EncodeGenericGrouped.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from .scalar import (
15
+ MISSING,
16
+ format_key,
17
+ format_scalar,
18
+ is_bare_key,
19
+ needs_quote,
20
+ parse_quoted_string,
21
+ parse_scalar,
22
+ quote_string,
23
+ split_respecting_quotes,
24
+ )
25
+
26
+
27
+ class FieldEntry:
28
+ """One parsed entry of a tabular field declaration. A plain field has only a
29
+ name. A constant column (SPEC 7.4.7) carries an unparsed value token after an
30
+ unquoted "=". A key column (SPEC 7.4.8.1, 10a.1) carries a leading "@"."""
31
+
32
+ __slots__ = ("name", "is_key", "is_const", "const_tok")
33
+
34
+ def __init__(self, name: str = "", is_key: bool = False, is_const: bool = False, const_tok: str = ""):
35
+ self.name = name
36
+ self.is_key = is_key
37
+ self.is_const = is_const
38
+ self.const_tok = const_tok
39
+
40
+
41
+ def _quoted_string_end(s: str) -> int:
42
+ """Index just past the closing quote of a quoted string starting at s[0], or -1
43
+ if unterminated."""
44
+ escaped = False
45
+ i = 1
46
+ while i < len(s):
47
+ if escaped:
48
+ escaped = False
49
+ i += 1
50
+ continue
51
+ if s[i] == "\\":
52
+ escaped = True
53
+ i += 1
54
+ continue
55
+ if s[i] == '"':
56
+ return i + 1
57
+ i += 1
58
+ return -1
59
+
60
+
61
+ def _split_name_value(r: str) -> tuple[str, str | None]:
62
+ """Parse a field entry's name and optional "=value" tail. The name is a Section
63
+ 2a key (bare or quoted); the "=" that introduces a constant value is the first
64
+ unquoted "=" after the (possibly quoted) name. A None value means the entry is a
65
+ plain field (no "=")."""
66
+ if r == "":
67
+ raise ValueError("malformed_header_field: empty field entry")
68
+ if r[0] == '"':
69
+ end = _quoted_string_end(r)
70
+ if end < 0:
71
+ raise ValueError("unterminated_quote: field name")
72
+ nm = parse_quoted_string(r[:end])
73
+ after = r[end:]
74
+ if after == "":
75
+ return nm, None
76
+ if after[0] == "=":
77
+ return nm, after[1:]
78
+ raise ValueError("malformed_header_field: unexpected characters after quoted field name")
79
+ idx = r.find("=")
80
+ if idx >= 0:
81
+ nm = r[:idx]
82
+ if nm == "":
83
+ raise ValueError("malformed_header_field: empty field name")
84
+ if not is_bare_key(nm):
85
+ raise ValueError(f"invalid field name: {nm}")
86
+ return nm, r[idx + 1:]
87
+ if not is_bare_key(r):
88
+ raise ValueError(f"invalid field name: {r}")
89
+ return r, None
90
+
91
+
92
+ def parse_field_entries(decl_str: str) -> list[FieldEntry]:
93
+ """Parse a {...} field declaration supporting "@" key markers and "name=value"
94
+ constant columns. Commas, and the "=" boundary, are parsed respecting quoted
95
+ names and quoted values (SPEC 7.4.7.2, mirroring 2a.3)."""
96
+ if len(decl_str) < 2 or decl_str[0] != "{" or decl_str[-1] != "}":
97
+ raise ValueError(f"invalid field declaration: {decl_str}")
98
+ inner = decl_str[1:-1]
99
+ if inner == "":
100
+ return []
101
+ raw = split_respecting_quotes(inner, ",")
102
+ entries: list[FieldEntry] = []
103
+ for r in raw:
104
+ r = r.strip()
105
+ e = FieldEntry()
106
+ if r.startswith("@"):
107
+ e.is_key = True
108
+ r = r[1:]
109
+ nm, val = _split_name_value(r)
110
+ e.name = nm
111
+ if val is not None:
112
+ e.is_const = True
113
+ e.const_tok = val
114
+ entries.append(e)
115
+ seen: set[str] = set()
116
+ for e in entries:
117
+ if e.name in seen:
118
+ raise ValueError(f"duplicate_field_name: {e.name}")
119
+ seen.add(e.name)
120
+ return entries
121
+
122
+
123
+ def parse_const_value(tok: str) -> Any:
124
+ """Parse a constant-column value token into a scalar (SPEC 7.4.7.2). The absent
125
+ marker and empty/attachment tokens are rejected."""
126
+ if tok == "":
127
+ raise ValueError("invalid_const_value: empty constant value (the empty string is always quoted)")
128
+ if tok == "~":
129
+ raise ValueError("invalid_const_value: absent marker ~ is not valid in a field declaration")
130
+ # Reject only a complete attachment marker, mirroring the encoder's Section 2.4
131
+ # quoting predicate (bare "^", or "^{...}" ending in "}"). A "^{"-prefixed token with
132
+ # no closing "}" (e.g. "^{abc") is a literal string the encoder leaves bare, so the
133
+ # decoder must accept it as a scalar.
134
+ if tok == "^" or (len(tok) >= 3 and tok[0] == "^" and tok[1] == "{" and tok[-1] == "}"):
135
+ raise ValueError("invalid_const_value: attachment marker is not a scalar")
136
+ return parse_scalar(tok, tabular_context=False)
137
+
138
+
139
+ def format_const_value(v: Any) -> str:
140
+ """Format a scalar as a constant-column header value (SPEC 7.4.7.2): the Section
141
+ 2.4 obligation plus quoting when the value contains "}" (the "," case is already
142
+ covered by needs_quote). Null is "-"."""
143
+ if v is None:
144
+ return "-"
145
+ if isinstance(v, str):
146
+ if needs_quote(v) or "}" in v:
147
+ return quote_string(v)
148
+ return v
149
+ return format_scalar(v)
150
+
151
+
152
+ def _path_top_level(name: str) -> tuple[str, bool]:
153
+ """Top-level group key of a flattened path column (SPEC 7.4.6) and True when the
154
+ name is a valid path (contains ">" with all segments non-empty), mirroring
155
+ _parse_tabular_body's path-column detection."""
156
+ if ">" not in name:
157
+ return "", False
158
+ parts = name.split(">")
159
+ for p in parts:
160
+ if p == "":
161
+ return "", False
162
+ return parts[0], True
163
+
164
+
165
+ def decode_constant_array(lines, header_line, depth, entries, count, parse_tabular_body):
166
+ """Parse a tabular array whose field declaration contains one or more constant
167
+ columns (SPEC 7.4.7). Parse the rows with the bare (per-record) fields only, then
168
+ rebuild each record in declaration order, inserting each constant at its position.
169
+ Returns (records, lines_consumed_including_header)."""
170
+ bare_fields: list[str] = []
171
+ const_vals: dict[str, Any] = {}
172
+ for e in entries:
173
+ if e.is_const:
174
+ const_vals[e.name] = parse_const_value(e.const_tok)
175
+ continue
176
+ bare_fields.append(e.name)
177
+ if not bare_fields:
178
+ raise ValueError("no_bare_column: every field is constant; a row must carry at least one per-record column")
179
+ rows, consumed = parse_tabular_body(lines, header_line + 1, depth, bare_fields, count)
180
+ if count >= 0 and len(rows) != count:
181
+ raise ValueError(f"count_mismatch: declared {count}, got {len(rows)}")
182
+
183
+ # Plan the output-key order over all entries, mirroring _parse_tabular_body: a bare
184
+ # path column (contains ">") collapses to its top-level key at the first occurrence,
185
+ # a plain field keeps its name, and a constant contributes its name at its position.
186
+ plan: list[tuple[str, bool]] = [] # (name, is_const)
187
+ in_plan: set[str] = set()
188
+ seen_group: set[str] = set()
189
+ for e in entries:
190
+ if e.is_const:
191
+ plan.append((e.name, True))
192
+ in_plan.add(e.name)
193
+ continue
194
+ top, ok = _path_top_level(e.name)
195
+ if ok:
196
+ if top not in seen_group:
197
+ seen_group.add(top)
198
+ plan.append((top, False))
199
+ in_plan.add(top)
200
+ continue
201
+ plan.append((e.name, False))
202
+ in_plan.add(e.name)
203
+
204
+ out: list[Any] = []
205
+ for r in rows:
206
+ rm = r if isinstance(r, dict) else {}
207
+ nm: dict[str, Any] = {}
208
+ for name, is_const in plan:
209
+ if is_const:
210
+ nm[name] = const_vals[name]
211
+ continue
212
+ if name in rm:
213
+ nm[name] = rm[name]
214
+ # Append any keys the record carries that were not in the plan (flatten-fallback
215
+ # attachments, Section 7.4.6.1.4), in the record's own order.
216
+ for k, v in rm.items():
217
+ if k not in in_plan:
218
+ nm[k] = v
219
+ out.append(nm)
220
+ return out, consumed + 1
221
+
222
+
223
+ def _parse_header_key(s: str) -> str:
224
+ """Parse a Section 2a key (bare or quoted) that occupies the whole of s."""
225
+ if s == "":
226
+ raise ValueError("empty key")
227
+ if s[0] == '"':
228
+ end = _quoted_string_end(s)
229
+ if end != len(s):
230
+ raise ValueError(f"malformed quoted key: {s}")
231
+ return parse_quoted_string(s)
232
+ if not is_bare_key(s):
233
+ raise ValueError(f"invalid key: {s}")
234
+ return s
235
+
236
+
237
+ def _index_unquoted_eq(s: str) -> int:
238
+ """Index of the first "=" outside a quoted string, or -1."""
239
+ in_quote = False
240
+ escaped = False
241
+ for i, c in enumerate(s):
242
+ if escaped:
243
+ escaped = False
244
+ continue
245
+ if c == "\\" and in_quote:
246
+ escaped = True
247
+ continue
248
+ if c == '"':
249
+ in_quote = not in_quote
250
+ continue
251
+ if c == "=" and not in_quote:
252
+ return i
253
+ return -1
254
+
255
+
256
+ def _parse_count_value(s: str) -> int:
257
+ """Count grammar (Section 4): decimal with no leading zero (except "0")."""
258
+ if s == "0":
259
+ return 0
260
+ if not s or s[0] == "0":
261
+ raise ValueError(f"invalid_count: {s}")
262
+ try:
263
+ n = int(s)
264
+ except ValueError:
265
+ raise ValueError(f"invalid_count: {s}")
266
+ if str(n) != s:
267
+ raise ValueError(f"invalid_count: {s}")
268
+ return n
269
+
270
+
271
+ def _parse_group_subheader(content: str) -> tuple[str, Any, int]:
272
+ """Parse a line of the form `{col}={value} [{count}]` (SPEC 7.4.8.3). The value
273
+ runs from the first unquoted "=" to the final " [" that begins the count. Returns
274
+ (col, value, count)."""
275
+ if not content.endswith("]"):
276
+ raise ValueError("invalid_group_header: subheader missing count bracket")
277
+ cnt_open = content.rfind(" [")
278
+ if cnt_open < 0:
279
+ raise ValueError("invalid_group_header: subheader missing count bracket")
280
+ count_str = content[cnt_open + 2: len(content) - 1]
281
+ try:
282
+ n = _parse_count_value(count_str)
283
+ except ValueError:
284
+ raise ValueError(f"invalid_count: {count_str}")
285
+ if n == 0:
286
+ raise ValueError("invalid_count: a group names at least one record")
287
+ col_eq_val = content[:cnt_open]
288
+ eq = _index_unquoted_eq(col_eq_val)
289
+ if eq < 0:
290
+ raise ValueError("invalid_group_header: subheader missing '='")
291
+ try:
292
+ col_name = _parse_header_key(col_eq_val[:eq])
293
+ except ValueError as err:
294
+ raise ValueError(f"invalid_group_header: {err}")
295
+ val_tok = col_eq_val[eq + 1:]
296
+ v = parse_const_value(val_tok)
297
+ return col_name, v, n
298
+
299
+
300
+ def decode_grouped_array(lines, header_line, depth, entries, group_clause, count):
301
+ """Parse a value-grouped tabular array (SPEC 7.4.8). group_clause is the trimmed
302
+ text after the field declaration's "}" (beginning with "group=").
303
+ Returns (records, lines_consumed)."""
304
+ if not group_clause.startswith("group="):
305
+ raise ValueError("invalid_group_header: malformed group clause")
306
+ try:
307
+ group_col = _parse_header_key(group_clause[len("group="):].strip())
308
+ except ValueError as err:
309
+ raise ValueError(f"invalid_group_header: {err}")
310
+
311
+ key_count = 0
312
+ key_name = ""
313
+ for e in entries:
314
+ if e.is_key:
315
+ key_count += 1
316
+ key_name = e.name
317
+ if key_count != 1:
318
+ raise ValueError("invalid_group_header: a grouped section requires exactly one @ key column")
319
+
320
+ # Validate the grouping column: present, not the key, not a constant column.
321
+ group_entry: FieldEntry | None = None
322
+ for e in entries:
323
+ if e.name == group_col:
324
+ group_entry = e
325
+ break
326
+ if group_entry is None:
327
+ raise ValueError(f'invalid_group_header: group column "{group_col}" is not a declared field')
328
+ if group_entry.is_key:
329
+ raise ValueError(f'invalid_group_header: group column "{group_col}" is the key column')
330
+ if group_entry.is_const:
331
+ raise ValueError(f'invalid_group_header: group column "{group_col}" is a constant column')
332
+
333
+ # Per-record (bare) fields are the non-constant fields other than the grouping
334
+ # column; the key column is included.
335
+ bare_fields: list[str] = []
336
+ const_vals: dict[str, Any] = {}
337
+ for e in entries:
338
+ if e.is_const:
339
+ const_vals[e.name] = parse_const_value(e.const_tok)
340
+ continue
341
+ if e.name == group_col:
342
+ continue
343
+ bare_fields.append(e.name)
344
+
345
+ indent = " " * depth
346
+ records: list[Any] = []
347
+ seen_groups: set[str] = set()
348
+ seen_keys: set[str] = set()
349
+ total = 0
350
+ i = header_line + 1
351
+ while i < len(lines):
352
+ content = lines[i]
353
+ if depth > 0:
354
+ if not content.startswith(indent):
355
+ break
356
+ content = content[len(indent):]
357
+ if content.startswith("## ") or content.startswith("##!"):
358
+ break
359
+
360
+ col, group_val, gcount = _parse_group_subheader(content)
361
+ if col != group_col:
362
+ raise ValueError(
363
+ f'invalid_group_header: subheader column "{col}" does not match group column "{group_col}"'
364
+ )
365
+ gkey = format_scalar(group_val)
366
+ if gkey in seen_groups:
367
+ raise ValueError(f"duplicate_group: {gkey}")
368
+ seen_groups.add(gkey)
369
+ i += 1
370
+
371
+ for _n in range(gcount):
372
+ if i >= len(lines):
373
+ raise ValueError(f'count_mismatch: group "{gkey}" declared {gcount} rows, found fewer')
374
+ row_content = lines[i]
375
+ if depth > 0:
376
+ if not row_content.startswith(indent):
377
+ raise ValueError(f'count_mismatch: group "{gkey}" declared {gcount} rows, found fewer')
378
+ row_content = row_content[len(indent):]
379
+ if row_content.startswith("## ") or row_content.startswith("##!"):
380
+ raise ValueError(f'count_mismatch: group "{gkey}" declared {gcount} rows, found fewer')
381
+ cells = split_respecting_quotes(row_content, "|")
382
+ if len(cells) != len(bare_fields):
383
+ raise ValueError(f"row_width_mismatch: expected {len(bare_fields)} fields, got {len(cells)}")
384
+ bare_vals: dict[str, Any] = {}
385
+ for j, f in enumerate(bare_fields):
386
+ cell = cells[j]
387
+ # Only a complete attachment marker (bare "^" or "^{...}" ending in "}")
388
+ # is forbidden; a "^{"-prefixed cell without a closing "}" is a literal
389
+ # row scalar, not an attachment.
390
+ if cell == "^" or (len(cell) >= 3 and cell[0] == "^" and cell[1] == "{" and cell[-1] == "}"):
391
+ raise ValueError("invalid_group_header: grouped records must not carry attachments")
392
+ pv = parse_scalar(cell, tabular_context=True)
393
+ if pv is MISSING:
394
+ continue
395
+ bare_vals[f] = pv
396
+ nm: dict[str, Any] = {}
397
+ for e in entries:
398
+ if e.name == group_col:
399
+ nm[e.name] = group_val
400
+ elif e.is_const:
401
+ nm[e.name] = const_vals[e.name]
402
+ else:
403
+ if e.name in bare_vals:
404
+ nm[e.name] = bare_vals[e.name]
405
+ if key_name not in nm:
406
+ raise ValueError(f'invalid_group_header: record missing key column "{key_name}"')
407
+ ks = format_scalar(nm[key_name])
408
+ if ks in seen_keys:
409
+ raise ValueError(f"duplicate_key: {ks}")
410
+ seen_keys.add(ks)
411
+ records.append(nm)
412
+ i += 1
413
+ total += gcount
414
+
415
+ if count >= 0 and total != count:
416
+ raise ValueError(f"count_mismatch: declared {count}, got {total}")
417
+ return records, i - header_line
418
+
419
+
420
+ def _tabular_fields(arr: list) -> list[str] | None:
421
+ if not arr:
422
+ return None
423
+ field_order: list[str] = []
424
+ seen: set[str] = set()
425
+ for item in arr:
426
+ if not isinstance(item, dict):
427
+ return None
428
+ for k in item:
429
+ if k not in seen:
430
+ field_order.append(k)
431
+ seen.add(k)
432
+ return field_order if field_order else None
433
+
434
+
435
+ def encode_generic_grouped(data: Any, key_field: str, group_field: str) -> str:
436
+ """Encode an array of uniform records as a value-grouped keyed set (SPEC 7.4.8):
437
+ opt-in, never the canonical default. key_field is the unique identity column
438
+ (emitted @-marked); group_field is the low-cardinality column the records are
439
+ clustered by. Other constant columns are factored (SPEC 7.4.7). It raises when the
440
+ array is not a keyed set the grammar can represent: a missing key/group field, a
441
+ non-unique key, key == group, or any record needing an attachment (nested value),
442
+ which grouped rows do not carry in this version.
443
+
444
+ The wire output is byte-identical to the Go reference EncodeGenericGrouped."""
445
+ if not isinstance(data, list):
446
+ raise ValueError("value-grouping requires a JSON array")
447
+ arr = data
448
+ if len(arr) == 0:
449
+ raise ValueError("value-grouping requires a non-empty array")
450
+ if key_field == group_field:
451
+ raise ValueError("value-grouping: key field and group field must differ")
452
+ for item in arr:
453
+ if not isinstance(item, dict):
454
+ raise ValueError("value-grouping requires an array of objects")
455
+ fields = _tabular_fields(arr)
456
+ if fields is None:
457
+ raise ValueError("value-grouping requires an array of objects with fields")
458
+ if key_field not in fields:
459
+ raise ValueError(f'value-grouping: key field "{key_field}" not present in the records')
460
+ if group_field not in fields:
461
+ raise ValueError(f'value-grouping: group field "{group_field}" not present in the records')
462
+
463
+ key_seen: set[str] = set()
464
+ for item in arr:
465
+ for f in fields:
466
+ if f not in item:
467
+ continue
468
+ val = item[f]
469
+ if isinstance(val, (dict, list)):
470
+ raise ValueError(f'value-grouping does not support nested values in this version: field "{f}"')
471
+ if key_field not in item or item[key_field] is None:
472
+ raise ValueError(f'value-grouping: key field "{key_field}" missing in a record')
473
+ ks = format_scalar(item[key_field])
474
+ if ks in key_seen:
475
+ raise ValueError(f'value-grouping: key field "{key_field}" is not unique ({ks})')
476
+ key_seen.add(ks)
477
+
478
+ # Constant columns (excluding key and group), factored per SPEC 7.4.7.
479
+ const_val: dict[str, str] = {}
480
+ if len(arr) >= 2:
481
+ for f in fields:
482
+ if f == key_field or f == group_field:
483
+ continue
484
+ first = ""
485
+ first_set = False
486
+ isc = True
487
+ for item in arr:
488
+ if f not in item:
489
+ isc = False
490
+ break
491
+ cv = format_const_value(item[f])
492
+ if not first_set:
493
+ first = cv
494
+ first_set = True
495
+ elif cv != first:
496
+ isc = False
497
+ break
498
+ if isc:
499
+ const_val[f] = first
500
+
501
+ header_fields: list[str] = []
502
+ for f in fields:
503
+ if f == key_field:
504
+ header_fields.append("@" + format_key(f))
505
+ elif f == group_field:
506
+ header_fields.append(format_key(f))
507
+ else:
508
+ if f in const_val:
509
+ header_fields.append(format_key(f) + "=" + const_val[f])
510
+ else:
511
+ header_fields.append(format_key(f))
512
+
513
+ bare_fields: list[str] = []
514
+ for f in fields:
515
+ if f == group_field:
516
+ continue
517
+ if f in const_val:
518
+ continue
519
+ bare_fields.append(f)
520
+
521
+ group_order: list[str] = []
522
+ group_members: dict[str, list[Any]] = {}
523
+ group_val_raw: dict[str, Any] = {}
524
+ for item in arr:
525
+ gv = item.get(group_field)
526
+ gk = format_scalar(gv)
527
+ if gk not in group_members:
528
+ group_order.append(gk)
529
+ group_members[gk] = []
530
+ group_val_raw[gk] = gv
531
+ group_members[gk].append(item)
532
+
533
+ out: list[str] = ["GCF profile=generic"]
534
+ out.append(f"## [{len(arr)}]{{{','.join(header_fields)}}} group={format_key(group_field)}")
535
+ for gk in group_order:
536
+ members = group_members[gk]
537
+ gv_str = format_scalar(group_val_raw[gk])
538
+ out.append(f"{format_key(group_field)}={gv_str} [{len(members)}]")
539
+ for item in members:
540
+ cells: list[str] = []
541
+ for f in bare_fields:
542
+ if f not in item:
543
+ cells.append("~")
544
+ elif item[f] is None:
545
+ cells.append("-")
546
+ else:
547
+ cells.append(format_scalar(item[f], "|"))
548
+ out.append("|".join(cells))
549
+ return "\n".join(out) + "\n"
gcf/decode_generic.py CHANGED
@@ -6,6 +6,11 @@ from typing import Any
6
6
 
7
7
  from .decode import decode
8
8
  from .keyed_map import keyed_rows_to_map
9
+ from .constant_grouping import (
10
+ decode_constant_array,
11
+ decode_grouped_array,
12
+ parse_field_entries,
13
+ )
9
14
  from .scalar import (
10
15
  parse_scalar, parse_quoted_string, split_respecting_quotes, split_field_decl,
11
16
  is_bare_key, MISSING, ATTACHMENT,
@@ -297,7 +302,36 @@ def _parse_array_from_header(
297
302
  brace_end = _find_closing_brace(after)
298
303
  if brace_end < 0:
299
304
  raise ValueError("invalid field declaration")
300
- fields = split_field_decl(after[:brace_end + 1])
305
+ decl_str = after[:brace_end + 1]
306
+ group_clause = after[brace_end + 1:].strip()
307
+
308
+ # Value-grouping (SPEC 7.4.8): non-keyed tabular array with a group= clause.
309
+ if not keyed and group_clause.startswith("group="):
310
+ entries = parse_field_entries(decl_str)
311
+ return decode_grouped_array(lines, header_line, depth, entries, group_clause, count)
312
+ if group_clause:
313
+ raise ValueError(f"malformed_header_field: unexpected content after field declaration: {group_clause}")
314
+
315
+ # Constant-column factoring (SPEC 7.4.7): a non-keyed tabular array whose field
316
+ # declaration carries name=value entries (or a stray @, which is valid only with
317
+ # a group= clause). The common case (no "=" or "@") takes the plain path.
318
+ if not keyed and ("=" in decl_str or "@" in decl_str):
319
+ entries = parse_field_entries(decl_str)
320
+ has_const = False
321
+ for e in entries:
322
+ if e.is_key:
323
+ raise ValueError(
324
+ f"invalid field name: @{e.name} (an @ key column is valid only in a grouped section)"
325
+ )
326
+ if e.is_const:
327
+ has_const = True
328
+ if has_const:
329
+ return decode_constant_array(
330
+ lines, header_line, depth, entries, count, _parse_tabular_body
331
+ )
332
+ # No constants after all (e.g. a quoted name containing "="): plain path.
333
+
334
+ fields = split_field_decl(decl_str)
301
335
  rows, consumed = _parse_tabular_body(lines, header_line + 1, depth, fields, count)
302
336
  if count >= 0 and len(rows) != count:
303
337
  raise ValueError(f"count_mismatch: declared {count}, got {len(rows)}")
gcf/generic.py CHANGED
@@ -7,6 +7,7 @@ from typing import Any
7
7
 
8
8
  from .scalar import format_scalar, format_key
9
9
  from .keyed_map import keyed_map_eligible
10
+ from .constant_grouping import format_const_value
10
11
 
11
12
 
12
13
  @dataclass
@@ -71,7 +72,7 @@ def _encode_root_array(arr: list, out: list[str], opts: GenericOptions) -> None:
71
72
  return
72
73
  fields = _tabular_fields(arr)
73
74
  if fields is not None:
74
- _encode_tabular("## ", arr, fields, out, 0, opts)
75
+ _encode_tabular("## ", arr, fields, out, 0, opts, factor_const=True)
75
76
  return
76
77
  _encode_expanded("## ", arr, out, 0, opts)
77
78
 
@@ -87,7 +88,7 @@ def _encode_named_array(name: str, arr: list, out: list[str], depth: int, opts:
87
88
  return
88
89
  fields = _tabular_fields(arr)
89
90
  if fields is not None:
90
- _encode_tabular(f"{prefix}## {name} ", arr, fields, out, depth, opts)
91
+ _encode_tabular(f"{prefix}## {name} ", arr, fields, out, depth, opts, factor_const=True)
91
92
  return
92
93
  _encode_expanded(f"{prefix}## {name} ", arr, out, depth, opts)
93
94
 
@@ -330,7 +331,7 @@ def _encode_keyed_map_with_prefix(
330
331
 
331
332
  def _encode_tabular(
332
333
  header_prefix: str, arr: list[dict], fields: list[str], out: list[str], depth: int,
333
- opts: GenericOptions, keyed: bool = False
334
+ opts: GenericOptions, keyed: bool = False, factor_const: bool = False
334
335
  ) -> None:
335
336
  prefix = _indent(depth)
336
337
 
@@ -376,7 +377,48 @@ def _encode_tabular(
376
377
  if sas is not None:
377
378
  shared_arr_schemas[f] = sas
378
379
 
379
- header_fields = ",".join(col["header"] for col in columns)
380
+ # Constant-column factoring (SPEC 7.4.7): a plain scalar column identical across
381
+ # every record is declared once in the header as name=value and omitted from the
382
+ # rows. Mandatory canonical for top-level tabular arrays, gated off for keyed maps
383
+ # and the nested-attachment path (factor_const). At least one per-record column
384
+ # remains.
385
+ const_col = [False] * len(columns)
386
+ const_header_val = [""] * len(columns)
387
+ if factor_const and not keyed and len(arr) >= 2:
388
+ for j, col in enumerate(columns):
389
+ if col["type"] != "original":
390
+ continue # only plain scalar columns qualify, never flattened/attachment
391
+ first = ""
392
+ first_set = False
393
+ is_const = True
394
+ for item in arr:
395
+ f = col["field"]
396
+ if f not in item:
397
+ is_const = False
398
+ break
399
+ v = item[f]
400
+ if isinstance(v, (dict, list)):
401
+ is_const = False
402
+ break
403
+ cv = format_const_value(v)
404
+ if not first_set:
405
+ first = cv
406
+ first_set = True
407
+ elif cv != first:
408
+ is_const = False
409
+ break
410
+ if is_const:
411
+ const_col[j] = True
412
+ const_header_val[j] = first
413
+ # At least one per-record column MUST remain. If every column is constant
414
+ # (an array of identical objects), leave the last union field unfactored.
415
+ if not any(not c for c in const_col):
416
+ const_col[len(columns) - 1] = False
417
+
418
+ header_fields = ",".join(
419
+ (col["header"] + "=" + const_header_val[i]) if const_col[i] else col["header"]
420
+ for i, col in enumerate(columns)
421
+ )
380
422
  br = ":]" if keyed else "]"
381
423
  out.append(f"{header_prefix}[{len(arr)}{br}{{{header_fields}}}")
382
424
 
@@ -437,7 +479,9 @@ def _encode_tabular(
437
479
  row_has_attachment = True
438
480
  attachments.append((f, item[f], False, None))
439
481
 
440
- row = "|".join(cells)
482
+ # Omit constant columns from the per-row cells (SPEC 7.4.7.3).
483
+ row_cells = [cells[j] for j in range(len(cells)) if not const_col[j]]
484
+ row = "|".join(row_cells)
441
485
  if row_has_attachment:
442
486
  out.append(f"{prefix}@{i} {row}")
443
487
  else:
@@ -1,17 +1,18 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gcf-python
3
- Version: 2.7.1
3
+ Version: 2.8.0
4
4
  Summary: The AI-native wire format for structured data. 50-92% fewer tokens than JSON, with multi-turn delta encoding for agent loops. 100% comprehension on every frontier model. Zero dependencies.
5
5
  Project-URL: Homepage, https://github.com/blackwell-systems/gcf-python
6
6
  Project-URL: Documentation, https://gcformat.com/
7
7
  Project-URL: Specification, https://github.com/blackwell-systems/gcf
8
8
  Author: Blackwell Systems
9
- License: MIT
9
+ License: Apache-2.0
10
10
  License-File: LICENSE
11
+ License-File: NOTICE
11
12
  Keywords: agents,delta,gcf,graph,llm,mcp,multi-turn,token-efficient,wire-format
12
13
  Classifier: Development Status :: 5 - Production/Stable
13
14
  Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: License :: OSI Approved :: Apache Software License
15
16
  Classifier: Programming Language :: Python :: 3
16
17
  Classifier: Programming Language :: Python :: 3.9
17
18
  Classifier: Programming Language :: Python :: 3.10
@@ -29,7 +30,7 @@ Description-Content-Type: text/markdown
29
30
  <a href="https://gcformat.com/playground.html"><img src="https://img.shields.io/badge/playground-live-2563eb?style=for-the-badge" alt="Playground"></a>
30
31
  <a href="https://gcformat.com/guide/benchmarks.html"><img src="https://img.shields.io/badge/benchmarks-2%2C500%2B%20evals-22c55e?style=for-the-badge" alt="Benchmarks"></a>
31
32
  <a href="https://pypi.org/project/gcf-python/"><img src="https://img.shields.io/pypi/v/gcf-python?style=for-the-badge&logo=python&logoColor=white&color=3776AB" alt="PyPI"></a>
32
- <a href="https://github.com/blackwell-systems/gcf-python/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-333?style=for-the-badge" alt="License"></a>
33
+ <a href="https://github.com/blackwell-systems/gcf-python/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache_2.0-333?style=for-the-badge" alt="License"></a>
33
34
  </p>
34
35
 
35
36
  <p align="center">
@@ -322,7 +323,7 @@ GCF wins 15/16 datasets on the expanded [token efficiency benchmark](https://git
322
323
 
323
324
  **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.
324
325
 
325
- **Specification:** [SPEC v3.5.3 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 281 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. Current versions: Go v1.7.1, TypeScript v2.6.1, Python v2.7.0, Rust v3.0.1, Swift v2.7.1, Kotlin v2.6.1, .NET v0.2.1. Cross-language conformance verified across all seven SDKs.
326
+ **Specification:** [SPEC v3.5.3 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 281 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. Current versions: Go v1.7.1, TypeScript v2.6.1, Python v2.7.1, Rust v3.0.1, Swift v2.7.1, Kotlin v2.6.1, .NET v0.2.1. Cross-language conformance verified across all seven SDKs.
326
327
 
327
328
  ## Adopted by
328
329
 
@@ -1,13 +1,14 @@
1
- gcf/__init__.py,sha256=uRYqyGTFSlmnfoj0hh79AZ_KhxiCgZgJt_v-okiK4_k,2616
1
+ gcf/__init__.py,sha256=0ulf234ior-OkGp4Yu_fqEKoXGENK1hk0yUov9rIuX0,2700
2
2
  gcf/__main__.py,sha256=EpvBz1yc8H0D5OJ1zy2tYke-kRzvudKa4DEbfeW14ao,71
3
3
  gcf/cli.py,sha256=UEe1CAZn-rKGNIo_ap8-oez3ucl6DSRbsdv6RDnzygY,5256
4
+ gcf/constant_grouping.py,sha256=OMdPF4aC1g2_mb16sbWPUhc4w9GHCqi6Y-EOYGdxfqo,21009
4
5
  gcf/constants.py,sha256=cmZ8YJSOB0im_eyfN8v4UvrLpBC6Fuf4cfcKZGbutxY,638
5
6
  gcf/decode.py,sha256=TP58_7UBhfeI0o9zpNgtQvAnLQGhNt0RaUjku7Ke75A,7162
6
- gcf/decode_generic.py,sha256=X59J2tV_ybZdWupVIVRtfLiqdxko2wfIn78Kkm5lNls,31170
7
+ gcf/decode_generic.py,sha256=iJlgPnq2L1M7PwxLQ8H0kBrtNiU31XItKDrrSV3Bazo,32762
7
8
  gcf/delta.py,sha256=oviQ9WsDRYXPXE8bw6SFutmzMRIvu-U_Zzw-39Nd5Ic,8053
8
9
  gcf/encode.py,sha256=OYGDyF3oP2I8Y2PrPL_5yDVURqB0mghtqlOGyDEd-Fs,4081
9
10
  gcf/fastmcp.py,sha256=cwIX77DhA96rMrelQcrlAlFXGQ4FDNModa4rSGlGN7I,5139
10
- gcf/generic.py,sha256=1c53utF2GcD8lS_wks2opQw08hMz4KGL-7C8g7JB5bA,21577
11
+ gcf/generic.py,sha256=V0Rl7vrZ64Zx3ryosg6a2mwgiT1EGIaAikV4Og3jZ50,23597
11
12
  gcf/generic_delta.py,sha256=5KSUT_QUeH7OILLWfu9JveaXRSrqRIi3c6E8WjlgC4Q,18785
12
13
  gcf/keyed_map.py,sha256=qCwhehXB1FQ2iYCPhcvXOqGwrVqdXhGYZGxv56CSUgc,3644
13
14
  gcf/packroot.py,sha256=0rZY7TEVcLzA8XhoIOc6_g9lbRCn-MUr54YTRVhzsO4,2019
@@ -16,8 +17,9 @@ gcf/session.py,sha256=rPqR4xsHKpi_G37t1ODwkTUdHhVAGa3Uig8F60h34Ms,5335
16
17
  gcf/stream.py,sha256=3eyzmLMZPyEGC1MS9pujxio2x_t7rCL-eNuJ4DjYkaA,5938
17
18
  gcf/stream_generic.py,sha256=h2mJ-jI-c1H2L0uoQVhrvDc7uJEcOwS1MB51k4SCPNM,5775
18
19
  gcf/types.py,sha256=AWm-LQoSqLHAYtEjcAxWQZqJ4JXqNreLUKO2mJFgNMA,1465
19
- gcf_python-2.7.1.dist-info/METADATA,sha256=FYQP8X41OUkaB4Bwyvl0rvUCmlMsdTfINUDiRqQMcLM,17107
20
- gcf_python-2.7.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
21
- gcf_python-2.7.1.dist-info/entry_points.txt,sha256=aFT6gqlkh8iGfM8cblE-LUMxHH08_v71IIoZtDdRIVA,37
22
- gcf_python-2.7.1.dist-info/licenses/LICENSE,sha256=2Fit9wnaIe--RMSAgyQqxC5hfZTyZqn4fIdBtp9qPDw,1072
23
- gcf_python-2.7.1.dist-info/RECORD,,
20
+ gcf_python-2.8.0.dist-info/METADATA,sha256=qugUJ_7q1XfsZnBtqBjvLlLHL2GCm9lOawNX5odLVrY,17154
21
+ gcf_python-2.8.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
22
+ gcf_python-2.8.0.dist-info/entry_points.txt,sha256=aFT6gqlkh8iGfM8cblE-LUMxHH08_v71IIoZtDdRIVA,37
23
+ gcf_python-2.8.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
24
+ gcf_python-2.8.0.dist-info/licenses/NOTICE,sha256=bIpAq8Yk3ThWC8AWHpS39kZPJ52bSrT_G0h9_yaGuR0,183
25
+ gcf_python-2.8.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,4 @@
1
+ gcf-python
2
+ Copyright 2026 Dayna Blackwell / Blackwell Systems
3
+
4
+ This product includes software developed by Dayna Blackwell / Blackwell Systems (https://github.com/blackwell-systems).
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Dayna Blackwell
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.