floe-python 0.8.4__py3-none-any.whl → 0.8.6__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.
- floe/checks/normalize.py +3 -3
- floe/config/validate.py +4 -1
- floe/iceberg/sink.py +2 -1
- floe/run/__init__.py +3 -3
- floe/run/entity.py +16 -4
- {floe_python-0.8.4.dist-info → floe_python-0.8.6.dist-info}/METADATA +1 -1
- {floe_python-0.8.4.dist-info → floe_python-0.8.6.dist-info}/RECORD +10 -10
- {floe_python-0.8.4.dist-info → floe_python-0.8.6.dist-info}/WHEEL +0 -0
- {floe_python-0.8.4.dist-info → floe_python-0.8.6.dist-info}/entry_points.txt +0 -0
- {floe_python-0.8.4.dist-info → floe_python-0.8.6.dist-info}/licenses/LICENSE +0 -0
floe/checks/normalize.py
CHANGED
|
@@ -94,12 +94,12 @@ def output_column_mapping(
|
|
|
94
94
|
targets: dict[str, str] = {}
|
|
95
95
|
mapping: dict[str, str] = {}
|
|
96
96
|
for column, target in zip(columns, output_names(columns, strategy), strict=True):
|
|
97
|
-
source = normalize_name(column.source or column.name, strategy)
|
|
98
97
|
if target in targets:
|
|
99
98
|
raise FloeConfigError(
|
|
100
|
-
f"output column name collision: {targets[target]} and {
|
|
99
|
+
f"output column name collision: {targets[target]} and {column.name} -> {target}"
|
|
101
100
|
)
|
|
102
|
-
targets[target] =
|
|
101
|
+
targets[target] = column.name
|
|
102
|
+
source = normalize_name(column.source or column.name, strategy)
|
|
103
103
|
if source != target:
|
|
104
104
|
mapping[source] = target
|
|
105
105
|
return mapping
|
floe/config/validate.py
CHANGED
|
@@ -6,6 +6,7 @@ from floe import __version__
|
|
|
6
6
|
from floe.checks.cast import decimal_type
|
|
7
7
|
from floe.checks.normalize import (
|
|
8
8
|
normalize_name,
|
|
9
|
+
output_column_mapping,
|
|
9
10
|
output_names,
|
|
10
11
|
resolve_strategy,
|
|
11
12
|
source_names,
|
|
@@ -416,7 +417,9 @@ def validate_schema(entity: EntityConfig, version: str) -> None:
|
|
|
416
417
|
f"(allowed: {', '.join(NORMALIZE_STRATEGIES)})",
|
|
417
418
|
)
|
|
418
419
|
try:
|
|
419
|
-
|
|
420
|
+
resolved = resolve_strategy(schema)
|
|
421
|
+
source_names(schema.columns, resolved)
|
|
422
|
+
output_column_mapping(schema.columns, resolved)
|
|
420
423
|
except FloeConfigError as err:
|
|
421
424
|
raise _fail(entity, str(err)) from err
|
|
422
425
|
mismatch = schema.mismatch
|
floe/iceberg/sink.py
CHANGED
|
@@ -177,7 +177,8 @@ def write_iceberg(
|
|
|
177
177
|
key = [output.get(k.strip(), k.strip()) for k in primary_key]
|
|
178
178
|
identifier = (target.namespace, target.table)
|
|
179
179
|
with run_error("rest catalog table_exists check failed"):
|
|
180
|
-
|
|
180
|
+
# the configured mode: a refreshed merge writes as overwrite and keeps the key guard (#1091)
|
|
181
|
+
exists = entity.sink.write_mode in ("merge_scd1", "merge_scd2") and catalog.table_exists(identifier) # fmt: skip
|
|
181
182
|
if exists:
|
|
182
183
|
with run_error("rest catalog load_table failed"):
|
|
183
184
|
existing = set(catalog.load_table(identifier).schema().column_names)
|
floe/run/__init__.py
CHANGED
|
@@ -72,10 +72,10 @@ def _rfc3339() -> str:
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
def _refresh(entity: EntityConfig) -> EntityConfig:
|
|
75
|
-
"""Full refresh: an append sink overwrites;
|
|
75
|
+
"""Full refresh: an append sink overwrites; `merge_scd1` overwrites in `run_entity`, which keeps its merge checks."""
|
|
76
76
|
mode = entity.sink.write_mode
|
|
77
|
-
if mode
|
|
78
|
-
raise FloeConfigError(f"entity '{entity.name}': --full-refresh is not supported with write_mode '{mode}'") # fmt: skip
|
|
77
|
+
if mode == "merge_scd2":
|
|
78
|
+
raise FloeConfigError(f"entity '{entity.name}': --full-refresh is not supported with write_mode '{mode}': its version history cannot be rebuilt from the inputs") # fmt: skip
|
|
79
79
|
if mode != "append":
|
|
80
80
|
return entity
|
|
81
81
|
return entity.model_copy(update={"sink": entity.sink.model_copy(update={"write_mode": "overwrite"})}) # fmt: skip
|
floe/run/entity.py
CHANGED
|
@@ -848,6 +848,11 @@ def _run_entity(
|
|
|
848
848
|
entity = with_partition_columns(
|
|
849
849
|
entity
|
|
850
850
|
) # D28: the derived columns are schema columns
|
|
851
|
+
merging = entity.sink.write_mode in ("merge_scd1", "merge_scd2")
|
|
852
|
+
configured = entity # the sink still checks a merge's key against the table
|
|
853
|
+
# #1091: a full refresh rebuilds a merge_scd1 table from every input in one commit
|
|
854
|
+
if inc.full_refresh and entity.sink.write_mode == "merge_scd1":
|
|
855
|
+
entity = entity.model_copy(update={"sink": entity.sink.model_copy(update={"write_mode": "overwrite"})}) # fmt: skip
|
|
851
856
|
sink = entity.sink
|
|
852
857
|
if sink.accepted.format not in ("parquet", "iceberg"):
|
|
853
858
|
raise FloeRunError(f"entity.name={entity.name} unsupported sink format: {sink.accepted.format}") # fmt: skip
|
|
@@ -885,7 +890,7 @@ def _run_entity(
|
|
|
885
890
|
if location and entity.incremental_mode in ("none", "archive"):
|
|
886
891
|
plan.archive = location[0], storage_client(location[1])
|
|
887
892
|
plan.root = source_root(source_uri) if entity.source.partitioning else None
|
|
888
|
-
if
|
|
893
|
+
if merging and entity.schema_.primary_key:
|
|
889
894
|
pk = [runtime.get(k.strip(), k.strip()) for k in entity.schema_.primary_key]
|
|
890
895
|
plan.merge_key = (
|
|
891
896
|
pk[0] if len(pk) == 1 else f"[{','.join(pk)}]"
|
|
@@ -928,6 +933,9 @@ def _run_entity(
|
|
|
928
933
|
skipped = len(files)
|
|
929
934
|
downloader = None if isinstance(client, LocalClient) else client
|
|
930
935
|
failure: list[Exception] = []
|
|
936
|
+
# #1090: a merge keeps a key's latest version, so the newest file is checked first; the first row of a file still wins.
|
|
937
|
+
# Not under abort: a duplicate aborts there anyway, and newer files committed ahead of a pending older one would let it merge stale rows later
|
|
938
|
+
latest_first = merging and entity.policy.severity != "abort"
|
|
931
939
|
|
|
932
940
|
def accepted_frames() -> Generator[pl.DataFrame]:
|
|
933
941
|
"""Files are read as the sink pulls (#1056); `files` and `complete` are final once it is exhausted."""
|
|
@@ -936,7 +944,7 @@ def _run_entity(
|
|
|
936
944
|
with tempfile.TemporaryDirectory(prefix="floe-") as temp_dir:
|
|
937
945
|
if pending and sink.write_mode == "append" and plan.tracker.keys and (target is None or plan.table is not None): # fmt: skip
|
|
938
946
|
plan.history = _key_cache(plan, accepted_client, base, Path(temp_dir)) # fmt: skip
|
|
939
|
-
for f in pending:
|
|
947
|
+
for f in pending[::-1] if latest_first else pending:
|
|
940
948
|
emit("file_started", entity=entity.name, input=f.source_uri)
|
|
941
949
|
timer = time.monotonic()
|
|
942
950
|
local = localize_input(f, downloader, Path(temp_dir))
|
|
@@ -973,7 +981,7 @@ def _run_entity(
|
|
|
973
981
|
write = write_accepted(frames, accepted_client, base, sink.write_mode, sink.accepted.options) # fmt: skip
|
|
974
982
|
else:
|
|
975
983
|
outputs = [plan.out_map.get(n, n) for n in names]
|
|
976
|
-
result, table, merge, added = write_iceberg(
|
|
984
|
+
result, table, merge, added = write_iceberg(configured, target, frames, outputs, sink.write_mode, emit.run_id, lambda: input_hash(entity, complete)) # fmt: skip
|
|
977
985
|
write = AcceptedWrite(result.files_written, result.file_paths, result.total_bytes_written,
|
|
978
986
|
result.avg_file_size_mb, result.small_files_count) # fmt: skip
|
|
979
987
|
if added:
|
|
@@ -989,11 +997,15 @@ def _run_entity(
|
|
|
989
997
|
raise failure[0] from failure[0].__cause__
|
|
990
998
|
finally:
|
|
991
999
|
stream.close()
|
|
1000
|
+
if latest_first: # the report lists the files in file order
|
|
1001
|
+
files[skipped:] = files[skipped:][::-1]
|
|
992
1002
|
files[skipped:] = sorted(files[skipped:], key=lambda r: r["input_file"] not in plan.prechecked) # fmt: skip
|
|
993
1003
|
inc.committed = files # #915: from here on a failure promotes the written files
|
|
994
1004
|
# #914: archive only once the sink committed, so a failed commit leaves the inputs in the source (Rust archives before)
|
|
995
1005
|
by_uri = {r["input_file"]: r for r in files}
|
|
996
|
-
for f in
|
|
1006
|
+
order = {f.source_uri: i for i, f in enumerate(inputs.files)}
|
|
1007
|
+
# #1090: oldest first, so a partial archive leaves the newest batch pending, never only an older one
|
|
1008
|
+
for f in sorted(plan.to_archive, key=lambda f: order[f.source_uri]) if latest_first else plan.to_archive: # fmt: skip
|
|
997
1009
|
by_uri[f.source_uri]["output"]["archived_path"] = _archive(plan, f)
|
|
998
1010
|
unique = [_report_names(r, key, runtime) for r, key in zip(plan.tracker.results(), keys, strict=True)] # fmt: skip
|
|
999
1011
|
report = build_report(
|
|
@@ -13,7 +13,7 @@ floe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
13
13
|
floe/state.py,sha256=XFHF6zVxbdGg1bkONZDLryTD4noDF9IgaimaEI-3Nlg,11104
|
|
14
14
|
floe/checks/cast.py,sha256=lM40xtKEggWI8FW3jijM3T2JFQtaXs8FhsFXo3vy5oM,6788
|
|
15
15
|
floe/checks/mismatch.py,sha256=2Gz-tiWKxy-iehSjAlsGzPLtYeNIfBWvi4yQPlASVYQ,5309
|
|
16
|
-
floe/checks/normalize.py,sha256=
|
|
16
|
+
floe/checks/normalize.py,sha256=4g-Mw-UOtLnitavD3lPWUUYweGqGMUx_8dhq2AJU8oc,4331
|
|
17
17
|
floe/checks/rows.py,sha256=E-YUZUvBUyOB7zat2dtbPWFM-0T1-01lYKuQU1A6wto,4156
|
|
18
18
|
floe/checks/unique.py,sha256=qoZVqS_uG_mWZlV7-LdxT6_dMVQRlklEvMutO_V0qaY,12396
|
|
19
19
|
floe/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -24,13 +24,13 @@ floe/config/profile.py,sha256=jWackztS-0H3dTMXHALTZ7LsIOmwjriwoeY4vFgARr0,16046
|
|
|
24
24
|
floe/config/project.py,sha256=zgTOhTDlbMr1chViMEZyv0drozZdumTaIoKDL0vv9sw,14141
|
|
25
25
|
floe/config/storage.py,sha256=BK3P65ukFqNgmbjCcuYmXR04OScf1yD8f19vGJq-aDI,6234
|
|
26
26
|
floe/config/types.py,sha256=4eveoOlyGmNxyZpI1PSjli9lnXLPc1-jYj4fL8qHN_8,5038
|
|
27
|
-
floe/config/validate.py,sha256=
|
|
27
|
+
floe/config/validate.py,sha256=7tyofwoTBL-BBCNxnmkXeSOybwmMNngeEcj8zviRyLs,36176
|
|
28
28
|
floe/config/vars.py,sha256=7Bq0IU0KrFeRlyA8W9-F0gDSDKfXCfFpBrX08H8Flvw,4577
|
|
29
29
|
floe/config/yaml.py,sha256=HeFGhKFxYkiqQOwljYOgUD5naJyxL_ZIX6hUIS36tzU,10071
|
|
30
30
|
floe/iceberg/catalog.py,sha256=n2NlgqRgnTe3Etdyra70Ib2aYtpR7zXWrajKl7spyYg,15340
|
|
31
31
|
floe/iceberg/compact.py,sha256=5O372vAIsvgRc2PGihryAsrQ0otn0F30AQu-cetRCP0,7123
|
|
32
32
|
floe/iceberg/merge.py,sha256=VE1Odf2B34Gxj7ZcuhbrfbgTGM9AdiDz6_2BbcQ-kSQ,12448
|
|
33
|
-
floe/iceberg/sink.py,sha256=
|
|
33
|
+
floe/iceberg/sink.py,sha256=WyT6yrp5JQyfaqWES96NWh3V-EzgTD4Y-IjB84mZ9Sg,11842
|
|
34
34
|
floe/iceberg/unique.py,sha256=OmiPQthhB5jGTaXsCu9ZztNFEFUf6eT0MLU2Gnh-C2A,1095
|
|
35
35
|
floe/iceberg/write.py,sha256=SRX5tb17zUeWwpg6uXKkVQ_muNJ2GqlQd0n-kmjWPVc,11141
|
|
36
36
|
floe/lineage/events.py,sha256=4txd8bGUwKBZd-8Aw_ebTurhlxAYk8SVMgH8vSjC3bs,6682
|
|
@@ -45,8 +45,8 @@ floe/read/xlsx.py,sha256=dengSY8HuNh9ahyR_LR8beqXTZQubqQ1Jx8qEak5jrY,3009
|
|
|
45
45
|
floe/read/xml.py,sha256=7m6DtoU8iExuLROavyojysqOUE_Bd_N0uwFaLAzjI_Y,8004
|
|
46
46
|
floe/report/entity.py,sha256=FRwLtXrlJXDNXrOKscitm-NCNJWQXd0vr_JIf_cUp20,9867
|
|
47
47
|
floe/report/summary.py,sha256=tb547VRkM5WGWHQ-4vZB2DUW4VTWgv9FUoym-NpuWE4,4452
|
|
48
|
-
floe/run/__init__.py,sha256=
|
|
49
|
-
floe/run/entity.py,sha256=
|
|
48
|
+
floe/run/__init__.py,sha256=Nm1OVIJF3p3DrkH3MCbBOzKcRFuFlMDSxhhdGyrM5N8,8189
|
|
49
|
+
floe/run/entity.py,sha256=xw9RPspLM9ZrY5CmQfk5qBJXcxUvO5aaosLs5UdOiPs,48710
|
|
50
50
|
floe/run/events.py,sha256=OE7zYkt0Dz2xrvgNavT1nHfZJ-97IIJ8LpHcameUJZU,2859
|
|
51
51
|
floe/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
52
|
floe/storage/archive.py,sha256=1gPImm9PDSJFCpWjg37ax4ubb0wwc6FZm38quQXhORg,1101
|
|
@@ -59,8 +59,8 @@ floe/storage/s3.py,sha256=XcG1qJTEY3KLSMLT950mUdrAxuCA8-Gx3fpNSzyV_Xo,7508
|
|
|
59
59
|
floe/storage/types.py,sha256=FvAEXWMdVdopRK276JyETRYGOva2-uR8pZvsWj6Xvy0,993
|
|
60
60
|
floe/write/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
61
|
floe/write/outputs.py,sha256=cXxGUIJnZ38XPaqEI6a83GFUsapBRdY52XQm3GlUPcI,6937
|
|
62
|
-
floe_python-0.8.
|
|
63
|
-
floe_python-0.8.
|
|
64
|
-
floe_python-0.8.
|
|
65
|
-
floe_python-0.8.
|
|
66
|
-
floe_python-0.8.
|
|
62
|
+
floe_python-0.8.6.dist-info/METADATA,sha256=eUy15Jwu4CdMPA1EJDxZYVGwz3gRcJN02PyqJW-5Qjo,442
|
|
63
|
+
floe_python-0.8.6.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
64
|
+
floe_python-0.8.6.dist-info/entry_points.txt,sha256=DU75r-E3zcK0quvwg5iT5DeWbUJck_Tz80bEj1IIzHA,40
|
|
65
|
+
floe_python-0.8.6.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
|
|
66
|
+
floe_python-0.8.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|