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 +2 -0
- gcf/constant_grouping.py +549 -0
- gcf/decode_generic.py +35 -1
- gcf/generic.py +49 -5
- {gcf_python-2.7.1.dist-info → gcf_python-2.8.0.dist-info}/METADATA +6 -5
- {gcf_python-2.7.1.dist-info → gcf_python-2.8.0.dist-info}/RECORD +10 -8
- {gcf_python-2.7.1.dist-info → gcf_python-2.8.0.dist-info}/WHEEL +1 -1
- gcf_python-2.8.0.dist-info/licenses/LICENSE +202 -0
- gcf_python-2.8.0.dist-info/licenses/NOTICE +4 -0
- gcf_python-2.7.1.dist-info/licenses/LICENSE +0 -21
- {gcf_python-2.7.1.dist-info → gcf_python-2.8.0.dist-info}/entry_points.txt +0 -0
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",
|
gcf/constant_grouping.py
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
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 ::
|
|
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-
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
20
|
-
gcf_python-2.
|
|
21
|
-
gcf_python-2.
|
|
22
|
-
gcf_python-2.
|
|
23
|
-
gcf_python-2.
|
|
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,,
|
|
@@ -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.
|
|
@@ -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.
|
|
File without changes
|