gcf-python 2.1.1__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gcf_python-2.1.1 → gcf_python-2.2.0}/CHANGELOG.md +11 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/PKG-INFO +2 -2
- {gcf_python-2.1.1 → gcf_python-2.2.0}/README.md +1 -1
- {gcf_python-2.1.1 → gcf_python-2.2.0}/pyproject.toml +1 -1
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/decode_generic.py +73 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/generic.py +145 -5
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_generic.py +5 -9
- {gcf_python-2.1.1 → gcf_python-2.2.0}/.github/FUNDING.yml +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/.github/workflows/ci.yml +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/.github/workflows/publish.yml +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/.gitignore +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/LICENSE +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/assets/gcf-python-diagram.png +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/__init__.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/__main__.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/cli.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/constants.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/decode.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/delta.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/encode.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/scalar.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/session.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/stream.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/stream_generic.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/src/gcf/types.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/__init__.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_conformance_v2.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_decode.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_delta.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_encode.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_roundtrip.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_roundtrip_v2.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_session.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_stream.py +0 -0
- {gcf_python-2.1.1 → gcf_python-2.2.0}/tests/test_stream_generic.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.2.0 (2026-06-22)
|
|
4
|
+
|
|
5
|
+
### Spec v3.2: Nested Object Flattening
|
|
6
|
+
|
|
7
|
+
- Encoder automatically flattens fixed-shape nested objects into `>` path column names (e.g., `"customer>name"` instead of `^` + `.customer {}` attachment)
|
|
8
|
+
- Decoder reconstructs nested objects from `>` path columns
|
|
9
|
+
- 20-48% fewer tokens on deeply nested API data (Jira, Stripe, K8s, calendar events)
|
|
10
|
+
- 100% comprehension on every frontier model (validated across 9 models, 7 providers)
|
|
11
|
+
- Zero regression on lossless round-trips (230 tests, conformance + property-based)
|
|
12
|
+
- Falls back to attachment mechanism for: variable-length arrays, objects with different keys across rows, objects with `>` in key names, empty nested objects
|
|
13
|
+
|
|
3
14
|
## v2.1.0 (2026-06-14)
|
|
4
15
|
|
|
5
16
|
### Spec v3.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gcf-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Drop-in JSON replacement for AI pipelines. 79% fewer tokens. 90.7% comprehension across 10 models. Zero dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/blackwell-systems/gcf-python
|
|
6
6
|
Project-URL: Documentation, https://blackwell-systems.github.io/gcf/
|
|
@@ -234,7 +234,7 @@ GCF wins 13/15 datasets on the expanded [token efficiency benchmark](https://git
|
|
|
234
234
|
| n8n | `npm install n8n-nodes-gcf` | [gcf-n8n-nodes](https://github.com/blackwell-systems/gcf-n8n-nodes) (workflow encode/decode) |
|
|
235
235
|
| Tree-sitter | `npm install tree-sitter-gcf` | [tree-sitter-gcf](https://github.com/blackwell-systems/tree-sitter-gcf) |
|
|
236
236
|
|
|
237
|
-
Zero runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
|
|
237
|
+
**Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
|
|
238
238
|
|
|
239
239
|
**Specification:** [SPEC v3.1 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 157 conformance fixtures, 33,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. All implementations at v2.1.0+ (Go v1.2.0). Cross-language 6x6 matrix verified.
|
|
240
240
|
|
|
@@ -209,7 +209,7 @@ GCF wins 13/15 datasets on the expanded [token efficiency benchmark](https://git
|
|
|
209
209
|
| n8n | `npm install n8n-nodes-gcf` | [gcf-n8n-nodes](https://github.com/blackwell-systems/gcf-n8n-nodes) (workflow encode/decode) |
|
|
210
210
|
| Tree-sitter | `npm install tree-sitter-gcf` | [tree-sitter-gcf](https://github.com/blackwell-systems/tree-sitter-gcf) |
|
|
211
211
|
|
|
212
|
-
Zero runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
|
|
212
|
+
**Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
|
|
213
213
|
|
|
214
214
|
**Specification:** [SPEC v3.1 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 157 conformance fixtures, 33,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. All implementations at v2.1.0+ (Go v1.2.0). Cross-language 6x6 matrix verified.
|
|
215
215
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gcf-python"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Drop-in JSON replacement for AI pipelines. 79% fewer tokens. 90.7% comprehension across 10 models. Zero dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -363,6 +363,55 @@ def _parse_attachment(
|
|
|
363
363
|
raise ValueError(f"invalid attachment form: {after_name}")
|
|
364
364
|
|
|
365
365
|
|
|
366
|
+
def _unflatten_paths(
|
|
367
|
+
path_columns: dict[str, list[str]],
|
|
368
|
+
flat_values: dict[str, Any],
|
|
369
|
+
flat_absent: set[str],
|
|
370
|
+
) -> dict[str, Any]:
|
|
371
|
+
"""Reconstruct nested objects from flat > path columns."""
|
|
372
|
+
groups: dict[str, list[str]] = {}
|
|
373
|
+
group_order: list[str] = []
|
|
374
|
+
for field_name, paths in path_columns.items():
|
|
375
|
+
if not paths:
|
|
376
|
+
continue
|
|
377
|
+
top = paths[0]
|
|
378
|
+
if top not in groups:
|
|
379
|
+
groups[top] = []
|
|
380
|
+
group_order.append(top)
|
|
381
|
+
groups[top].append(field_name)
|
|
382
|
+
|
|
383
|
+
result: dict[str, Any] = {}
|
|
384
|
+
|
|
385
|
+
for top in group_order:
|
|
386
|
+
field_names = groups[top]
|
|
387
|
+
all_absent = all(f in flat_absent for f in field_names)
|
|
388
|
+
all_null = all(
|
|
389
|
+
(f not in flat_absent and flat_values.get(f) is None)
|
|
390
|
+
for f in field_names
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if all_absent:
|
|
394
|
+
continue
|
|
395
|
+
if all_null:
|
|
396
|
+
result[top] = None
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
for field_name in field_names:
|
|
400
|
+
if field_name in flat_absent:
|
|
401
|
+
continue
|
|
402
|
+
paths = path_columns[field_name]
|
|
403
|
+
val = flat_values.get(field_name)
|
|
404
|
+
|
|
405
|
+
current = result
|
|
406
|
+
for k in paths[:-1]:
|
|
407
|
+
if k not in current:
|
|
408
|
+
current[k] = {}
|
|
409
|
+
current = current[k]
|
|
410
|
+
current[paths[-1]] = val
|
|
411
|
+
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
|
|
366
415
|
def _parse_tabular_body(
|
|
367
416
|
lines: list[str], start: int, depth: int, fields: list[str], expected_count: int
|
|
368
417
|
) -> tuple[list[Any], int]:
|
|
@@ -370,6 +419,12 @@ def _parse_tabular_body(
|
|
|
370
419
|
rows: list[Any] = []
|
|
371
420
|
i = start
|
|
372
421
|
|
|
422
|
+
# Detect path columns: fields containing ">".
|
|
423
|
+
path_column_map: dict[str, list[str]] = {}
|
|
424
|
+
for f in fields:
|
|
425
|
+
if ">" in f:
|
|
426
|
+
path_column_map[f] = f.split(">")
|
|
427
|
+
|
|
373
428
|
# Track inline schemas and shared array schemas.
|
|
374
429
|
inline_schemas: dict[str, list[str]] = {}
|
|
375
430
|
shared_array_schemas: dict[str, list[str]] = {}
|
|
@@ -409,9 +464,22 @@ def _parse_tabular_body(
|
|
|
409
464
|
inline_att_order: list[str] = []
|
|
410
465
|
missing_fields: set[str] = set()
|
|
411
466
|
|
|
467
|
+
# Collect path column values for unflattening.
|
|
468
|
+
flat_values: dict[str, Any] = {}
|
|
469
|
+
flat_absent: set[str] = set()
|
|
470
|
+
|
|
412
471
|
for j, f in enumerate(fields):
|
|
413
472
|
cell_val = vals[j]
|
|
414
473
|
|
|
474
|
+
# Path columns: store values for later unflattening.
|
|
475
|
+
if f in path_column_map:
|
|
476
|
+
parsed = parse_scalar(cell_val, tabular_context=True)
|
|
477
|
+
if parsed is MISSING:
|
|
478
|
+
flat_absent.add(f)
|
|
479
|
+
else:
|
|
480
|
+
flat_values[f] = parsed
|
|
481
|
+
continue
|
|
482
|
+
|
|
415
483
|
# Check for ^{fields} inline schema declaration.
|
|
416
484
|
if cell_val.startswith("^{") and cell_val.endswith("}"):
|
|
417
485
|
schema_str = cell_val[1:]
|
|
@@ -548,6 +616,11 @@ def _parse_tabular_body(
|
|
|
548
616
|
row[f] = cell_values[f]
|
|
549
617
|
elif f in attachment_values:
|
|
550
618
|
row[f] = attachment_values[f]
|
|
619
|
+
# Unflatten path columns into nested objects.
|
|
620
|
+
if path_column_map:
|
|
621
|
+
nested = _unflatten_paths(path_column_map, flat_values, flat_absent)
|
|
622
|
+
row.update(nested)
|
|
623
|
+
|
|
551
624
|
rows.append(row)
|
|
552
625
|
|
|
553
626
|
if expected_count >= 0 and len(rows) >= expected_count:
|
|
@@ -145,15 +145,136 @@ def _shared_array_schema(arr: list[dict], field_name: str) -> list[str] | None:
|
|
|
145
145
|
return canonical_fields
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
# ── Nested object flattening (v3.2) ──────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _analyze_flattenable(
|
|
152
|
+
arr: list[dict], field_name: str, parent_path: str
|
|
153
|
+
) -> list[dict] | None:
|
|
154
|
+
"""Analyze whether a field can be flattened. Returns list of leaf descriptors or None."""
|
|
155
|
+
canonical_shape: dict[str, str] | None = None # key -> "scalar" | "nested"
|
|
156
|
+
|
|
157
|
+
for item in arr:
|
|
158
|
+
if field_name not in item or item[field_name] is None:
|
|
159
|
+
continue
|
|
160
|
+
v = item[field_name]
|
|
161
|
+
if not isinstance(v, dict):
|
|
162
|
+
return None
|
|
163
|
+
if isinstance(v, list):
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
keys = list(v.keys())
|
|
167
|
+
|
|
168
|
+
if canonical_shape is None:
|
|
169
|
+
canonical_shape = {}
|
|
170
|
+
for k in keys:
|
|
171
|
+
if ">" in k:
|
|
172
|
+
return None
|
|
173
|
+
val = v[k]
|
|
174
|
+
if isinstance(val, list):
|
|
175
|
+
return None
|
|
176
|
+
elif isinstance(val, dict):
|
|
177
|
+
canonical_shape[k] = "nested"
|
|
178
|
+
else:
|
|
179
|
+
canonical_shape[k] = "scalar"
|
|
180
|
+
else:
|
|
181
|
+
if len(keys) != len(canonical_shape):
|
|
182
|
+
return None
|
|
183
|
+
for k in keys:
|
|
184
|
+
if k not in canonical_shape:
|
|
185
|
+
return None
|
|
186
|
+
val = v[k]
|
|
187
|
+
expected = canonical_shape[k]
|
|
188
|
+
if expected == "scalar":
|
|
189
|
+
if isinstance(val, (dict, list)):
|
|
190
|
+
return None
|
|
191
|
+
elif expected == "nested":
|
|
192
|
+
if isinstance(val, list):
|
|
193
|
+
return None
|
|
194
|
+
if val is not None and not isinstance(val, dict):
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
if canonical_shape is None:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
current_path = f"{parent_path}>{field_name}" if parent_path else field_name
|
|
201
|
+
parent_keys = parent_path.split(">") + [field_name] if parent_path else [field_name]
|
|
202
|
+
|
|
203
|
+
leaves: list[dict] = []
|
|
204
|
+
for k in canonical_shape:
|
|
205
|
+
if canonical_shape[k] == "scalar":
|
|
206
|
+
leaves.append({"path": f"{current_path}>{k}", "keys": parent_keys + [k]})
|
|
207
|
+
else:
|
|
208
|
+
sub_arr = []
|
|
209
|
+
for item in arr:
|
|
210
|
+
if field_name not in item or item[field_name] is None:
|
|
211
|
+
sub_arr.append({})
|
|
212
|
+
else:
|
|
213
|
+
sub_arr.append(item[field_name])
|
|
214
|
+
sub_leaves = _analyze_flattenable(sub_arr, k, current_path)
|
|
215
|
+
if sub_leaves is None or len(sub_leaves) == 0:
|
|
216
|
+
return None
|
|
217
|
+
leaves.extend(sub_leaves)
|
|
218
|
+
|
|
219
|
+
# Guard: reject if any row has non-null object with all-null leaves.
|
|
220
|
+
if leaves:
|
|
221
|
+
for item in arr:
|
|
222
|
+
if field_name not in item or item[field_name] is None:
|
|
223
|
+
continue
|
|
224
|
+
all_null = all(
|
|
225
|
+
_resolve_key_chain(item, leaf["keys"])[0] is None
|
|
226
|
+
and _resolve_key_chain(item, leaf["keys"])[1]
|
|
227
|
+
for leaf in leaves
|
|
228
|
+
)
|
|
229
|
+
if all_null:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
return leaves
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _resolve_key_chain(item: Any, keys: list[str]) -> tuple[Any, bool]:
|
|
236
|
+
"""Traverse an object by key chain. Returns (value, exists)."""
|
|
237
|
+
if not keys or not isinstance(item, dict):
|
|
238
|
+
return None, False
|
|
239
|
+
if keys[0] not in item:
|
|
240
|
+
return None, False
|
|
241
|
+
current = item[keys[0]]
|
|
242
|
+
if current is None:
|
|
243
|
+
return None, True
|
|
244
|
+
for k in keys[1:]:
|
|
245
|
+
if not isinstance(current, dict) or k not in current:
|
|
246
|
+
return None, False
|
|
247
|
+
current = current[k]
|
|
248
|
+
return current, True
|
|
249
|
+
|
|
250
|
+
|
|
148
251
|
def _encode_tabular(
|
|
149
252
|
header_prefix: str, arr: list[dict], fields: list[str], out: list[str], depth: int
|
|
150
253
|
) -> None:
|
|
151
254
|
prefix = _indent(depth)
|
|
152
255
|
|
|
153
|
-
#
|
|
256
|
+
# Phase 0: Analyze fields for flattening.
|
|
257
|
+
flatten_map: dict[str, list[dict]] = {}
|
|
258
|
+
for f in fields:
|
|
259
|
+
leaves = _analyze_flattenable(arr, f, "")
|
|
260
|
+
if leaves and len(leaves) > 0:
|
|
261
|
+
flatten_map[f] = leaves
|
|
262
|
+
|
|
263
|
+
# Build expanded column list.
|
|
264
|
+
columns: list[dict] = []
|
|
265
|
+
for f in fields:
|
|
266
|
+
if f in flatten_map:
|
|
267
|
+
for leaf in flatten_map[f]:
|
|
268
|
+
columns.append({"header": format_key(leaf["path"]), "type": "flat", "field": f, "keys": leaf["keys"]})
|
|
269
|
+
else:
|
|
270
|
+
columns.append({"header": format_key(f), "type": "original", "field": f, "keys": []})
|
|
271
|
+
|
|
272
|
+
# Pre-compute inline schemas and shared array schemas (skip flattened fields).
|
|
154
273
|
inline_schemas: dict[str, list[str]] = {}
|
|
155
274
|
shared_arr_schemas: dict[str, list[str]] = {}
|
|
156
275
|
for f in fields:
|
|
276
|
+
if f in flatten_map:
|
|
277
|
+
continue
|
|
157
278
|
ifs = _inline_schema_fields(arr, f)
|
|
158
279
|
if ifs is not None:
|
|
159
280
|
inline_schemas[f] = ifs
|
|
@@ -161,15 +282,34 @@ def _encode_tabular(
|
|
|
161
282
|
if sas is not None:
|
|
162
283
|
shared_arr_schemas[f] = sas
|
|
163
284
|
|
|
164
|
-
|
|
165
|
-
out.append(f"{header_prefix}[{len(arr)}]{{{
|
|
285
|
+
header_fields = ",".join(col["header"] for col in columns)
|
|
286
|
+
out.append(f"{header_prefix}[{len(arr)}]{{{header_fields}}}")
|
|
166
287
|
|
|
167
288
|
for i, item in enumerate(arr):
|
|
168
289
|
cells: list[str] = []
|
|
169
|
-
attachments: list[tuple[str, Any, bool, list[str] | None]] = []
|
|
290
|
+
attachments: list[tuple[str, Any, bool, list[str] | None]] = []
|
|
170
291
|
row_has_attachment = False
|
|
171
292
|
|
|
172
|
-
for
|
|
293
|
+
for col in columns:
|
|
294
|
+
if col["type"] == "flat":
|
|
295
|
+
keys = col["keys"]
|
|
296
|
+
if keys[0] not in item:
|
|
297
|
+
cells.append("~")
|
|
298
|
+
else:
|
|
299
|
+
top_val = item[keys[0]]
|
|
300
|
+
if top_val is None:
|
|
301
|
+
cells.append("-")
|
|
302
|
+
else:
|
|
303
|
+
val, exists = _resolve_key_chain(item, keys)
|
|
304
|
+
if not exists:
|
|
305
|
+
cells.append("~")
|
|
306
|
+
elif val is None:
|
|
307
|
+
cells.append("-")
|
|
308
|
+
else:
|
|
309
|
+
cells.append(format_scalar(val, "|"))
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
f = col["field"]
|
|
173
313
|
if f not in item:
|
|
174
314
|
cells.append("~")
|
|
175
315
|
continue
|
|
@@ -60,15 +60,11 @@ def test_encode_mixed_data():
|
|
|
60
60
|
}
|
|
61
61
|
output = encode_generic(data)
|
|
62
62
|
|
|
63
|
-
# Header includes
|
|
64
|
-
assert
|
|
65
|
-
# Rows with
|
|
66
|
-
assert "
|
|
67
|
-
assert "
|
|
68
|
-
# Nested config uses .field {} attachment.
|
|
69
|
-
assert ".config {}" in output
|
|
70
|
-
assert "env=" in output
|
|
71
|
-
assert "region=" in output
|
|
63
|
+
# Header includes flattened path columns (v3.2).
|
|
64
|
+
assert '## projects [2]{name,status,"config>env","config>region"}' in output
|
|
65
|
+
# Rows with flattened data are pure tabular.
|
|
66
|
+
assert "Alpha|active|prod|us-east" in output
|
|
67
|
+
assert "Beta|draft|staging|eu-west" in output
|
|
72
68
|
|
|
73
69
|
|
|
74
70
|
def test_encode_none_value():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|