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 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 {source} -> {target}"
99
+ f"output column name collision: {targets[target]} and {column.name} -> {target}"
101
100
  )
102
- targets[target] = source
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
- source_names(schema.columns, resolve_strategy(schema))
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
- exists = mode in ("merge_scd1", "merge_scd2") and catalog.table_exists(identifier) # fmt: skip
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; merge sinks cannot refresh."""
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 in ("merge_scd1", "merge_scd2"):
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 sink.write_mode in ("merge_scd1", "merge_scd2") and entity.schema_.primary_key:
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(entity, target, frames, outputs, sink.write_mode, emit.run_id, lambda: input_hash(entity, complete)) # fmt: skip
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 plan.to_archive:
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: floe-python
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: Floe: a data-contract runtime for file ingestion
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -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=8ZfpGFauFLIrq_xHY3iIe-rtuKO88YACDVO0WQJFWCU,4321
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=4lU6yJ1qpI8AIr42SGy-xJR2brz_DBG6pNS_dkZw4zo,36065
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=ySEAsKYpaKnqF9t7WjOW03KwpKynNIqLxOP1kQ-0Tbg,11723
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=6teCk4GZoX7grwtyhQUbqwbx_TSQebzViUJA19CmEl4,8107
49
- floe/run/entity.py,sha256=dML8VR-UyLUNVupkKaHeFIbR06TdPP2qcJ2rAMW4GeU,47568
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.4.dist-info/METADATA,sha256=kGo2pIkok-Gpky0VSm-2S5ttVTHx3tDcE21ynZE-HM4,442
63
- floe_python-0.8.4.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
64
- floe_python-0.8.4.dist-info/entry_points.txt,sha256=DU75r-E3zcK0quvwg5iT5DeWbUJck_Tz80bEj1IIzHA,40
65
- floe_python-0.8.4.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
66
- floe_python-0.8.4.dist-info/RECORD,,
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,,