floe-python 0.8.5__py3-none-any.whl → 0.8.7__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/__init__.py CHANGED
@@ -46,6 +46,7 @@ from floe.run.events import (
46
46
  )
47
47
  from floe.state import dump_state, read_state, reset
48
48
  from floe.storage.paths import normalize_local_path
49
+ from floe.storage.resolve import clear_storage_clients
49
50
  from floe.storage.types import StorageClient
50
51
 
51
52
  _observer: Callable[[RunEvent], None] | None = None
@@ -281,6 +282,7 @@ def extract_config_env_vars(config_path: str) -> dict[str, str]:
281
282
 
282
283
 
283
284
  def _state(config_path: str, entity_name: str) -> tuple[str, str, str, StorageClient]:
285
+ clear_storage_clients()
284
286
  path, root = _load(config_path)
285
287
  [entity] = _run.select_entities(root, [entity_name])
286
288
  uri, client = state_uri(entity, normalize_local_path(path.parent), root.storages)
floe/checks/normalize.py CHANGED
@@ -115,4 +115,6 @@ def normalize_columns(df: pl.DataFrame, strategy: str | None) -> pl.DataFrame:
115
115
  f"normalized input column collision: {seen[new]} and {old} -> {new}"
116
116
  )
117
117
  seen[new] = old
118
- return df.rename(names)
118
+ renamed = df.clone()
119
+ renamed.columns = list(names.values()) # a rename is a query plan, this is not
120
+ return renamed
floe/checks/rows.py CHANGED
@@ -109,6 +109,10 @@ def split_rows(
109
109
  sources: Mapping[str, str] | None = None,
110
110
  ) -> tuple[pl.DataFrame, pl.DataFrame]:
111
111
  """`(accepted, rejected)`; rejected adds `__floe_row_index` (UInt64, row position in `df`) and `__floe_errors`."""
112
+ # a null mask passes the row, as in `error_column`
113
+ if not any(c.mask.any() for c in checks):
114
+ none = [pl.Series(ROW_INDEX, [], pl.UInt64), pl.Series(ERRORS, [], pl.String)]
115
+ return df, df.clear().with_columns(none) # replaces a clashing column, as below
112
116
  errors = error_column(df.height, checks, formatter, sources)
113
117
  rejected = df.with_columns(
114
118
  pl.int_range(pl.len(), dtype=pl.UInt64).alias(ROW_INDEX), errors
floe/iceberg/catalog.py CHANGED
@@ -4,6 +4,7 @@ import os
4
4
  import re
5
5
  import shutil
6
6
  import tempfile
7
+ import threading
7
8
  import weakref
8
9
  from collections.abc import Generator, Sequence
9
10
  from contextlib import contextmanager
@@ -83,8 +84,34 @@ def _credential_props(catalog: str, credential: str) -> dict[str, str]:
83
84
  return {"credential": credential.removeprefix("client_credentials:")}
84
85
 
85
86
 
87
+ _CATALOGS: dict[tuple[CatalogDefinition, StorageDefinition | None], Catalog] = {}
88
+ _CATALOGS_LOCK = threading.Lock()
89
+
90
+
91
+ def clear_catalogs() -> None:
92
+ """Called when a run starts: a catalog is reused within a run only, never across runs (#1101)."""
93
+ with _CATALOGS_LOCK:
94
+ _CATALOGS.clear()
95
+
96
+
86
97
  def open_catalog(
87
98
  definition: CatalogDefinition, storage: StorageDefinition | None = None
99
+ ) -> Catalog:
100
+ """One catalog per definition and storage until `clear_catalogs`, so the REST init (config, OAuth token) happens once.
101
+
102
+ Safe to call from several threads (one init per key). PyIceberg 0.12 refetches an expired OAuth token
103
+ itself: its REST calls retry once on a 401 after a new client-credentials request; a static `token:` has
104
+ nothing to refetch, as with a freshly opened catalog. A failed init is not kept.
105
+ """
106
+ key = (definition, storage)
107
+ with _CATALOGS_LOCK:
108
+ if key not in _CATALOGS:
109
+ _CATALOGS[key] = _open_catalog(definition, storage)
110
+ return _CATALOGS[key]
111
+
112
+
113
+ def _open_catalog(
114
+ definition: CatalogDefinition, storage: StorageDefinition | None
88
115
  ) -> Catalog:
89
116
  """Credentials for S3 and Glue come from the environment; the catalog never vends them (as in Rust)."""
90
117
  props = {
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/read/csv.py CHANGED
@@ -79,15 +79,16 @@ def _scan_args(options: SourceOptions) -> _Args:
79
79
 
80
80
  def _columns(path: Path, args: _Args, declared: Sequence[str]) -> list[str]:
81
81
  try:
82
- probe = args.scan(path, n_rows=0 if args.has_header else 1).collect()
82
+ probe = args.scan(path, n_rows=0 if args.has_header else 1)
83
+ if args.has_header: # the schema is the header: no plan to run
84
+ return probe.collect_schema().names()
85
+ width = probe.collect().width
83
86
  except (pl.exceptions.PolarsError, OSError) as err:
84
87
  # Rust's polars prints the error kind: `no data: empty CSV`
85
88
  kind = "no data: " if isinstance(err, pl.exceptions.NoDataError) else ""
86
89
  raise FloeIoError(f"csv header read failed: {kind}{err}") from err
87
- if args.has_header:
88
- return probe.columns
89
- names = list(declared[: probe.width])
90
- return names + [f"extra_column_{i + 1}" for i in range(len(declared), probe.width)]
90
+ names = list(declared[:width])
91
+ return names + [f"extra_column_{i + 1}" for i in range(len(declared), width)]
91
92
 
92
93
 
93
94
  def _field_counts(path: Path, args: _Args, skip: int) -> Iterator[int]:
floe/run/__init__.py CHANGED
@@ -11,6 +11,7 @@ from floe.checks.rows import Formatter
11
11
  from floe.config.project import RootConfig
12
12
  from floe.config.types import EntityConfig
13
13
  from floe.errors import FloeConfigError, FloeError
14
+ from floe.iceberg.catalog import clear_catalogs
14
15
  from floe.report.summary import (
15
16
  build_summary,
16
17
  id_path,
@@ -24,7 +25,7 @@ from floe.run.entity import EntityRun, list_source, resolve_source, run_entity
24
25
  from floe.run.events import Emitter, Observer
25
26
  from floe.storage.inputs import ResolvedInputs
26
27
  from floe.storage.paths import normalize_local_path
27
- from floe.storage.resolve import resolve_path, storage_client
28
+ from floe.storage.resolve import clear_storage_clients, resolve_path, storage_client
28
29
 
29
30
 
30
31
  @dataclass(frozen=True)
@@ -72,10 +73,10 @@ def _rfc3339() -> str:
72
73
 
73
74
 
74
75
  def _refresh(entity: EntityConfig) -> EntityConfig:
75
- """Full refresh: an append sink overwrites; merge sinks cannot refresh."""
76
+ """Full refresh: an append sink overwrites; `merge_scd1` overwrites in `run_entity`, which keeps its merge checks."""
76
77
  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
78
+ if mode == "merge_scd2":
79
+ 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
80
  if mode != "append":
80
81
  return entity
81
82
  return entity.model_copy(update={"sink": entity.sink.model_copy(update={"write_mode": "overwrite"})}) # fmt: skip
@@ -104,7 +105,9 @@ def run(
104
105
  config_uri: str | None = None,
105
106
  ) -> RunOutcome:
106
107
  """Run the selected entities in config order, writing each entity's report as it finishes. A failing entity raises: no summary, as Rust."""
108
+ clear_storage_clients()
107
109
  started, timer = _rfc3339(), time.monotonic()
110
+ clear_catalogs()
108
111
  selected = select_entities(root, entities)
109
112
  if full_refresh:
110
113
  selected = [_refresh(e) for e in selected]
floe/run/entity.py CHANGED
@@ -5,7 +5,7 @@ import tempfile
5
5
  import threading
6
6
  import time
7
7
  from collections.abc import Callable, Generator, Iterable, Iterator
8
- from contextlib import suppress
8
+ from contextlib import closing, suppress
9
9
  from dataclasses import dataclass, field, replace
10
10
  from datetime import datetime
11
11
  from functools import partial
@@ -85,7 +85,6 @@ from floe.storage.inputs import (
85
85
  InputFile,
86
86
  NoInputsError,
87
87
  ResolvedInputs,
88
- localize_input,
89
88
  resolve_inputs,
90
89
  suffixes_for_format,
91
90
  )
@@ -99,6 +98,7 @@ from floe.storage.partitioning import (
99
98
  window,
100
99
  )
101
100
  from floe.storage.paths import normalize_local_path
101
+ from floe.storage.prefetch import localized_inputs
102
102
  from floe.storage.resolve import resolve_path, storage_client
103
103
  from floe.storage.s3 import S3Client
104
104
  from floe.storage.types import StorageClient
@@ -335,27 +335,31 @@ def _typed(
335
335
  ) -> tuple[pl.DataFrame, list[RowCheck]]:
336
336
  entity, src = plan.entity, plan.entity.source
337
337
  # D28 (#913): the derived columns join after the file's own columns are normalised and matched
338
- df = apply_mismatch(normalize_columns(raw, plan.strategy), mismatch).with_columns(
339
- derived
340
- )
341
- df = df.select(
342
- pl.col(n) if n in df.columns else pl.lit(None, pl.String).alias(n)
343
- for n in plan.names
344
- )
338
+ df = apply_mismatch(normalize_columns(raw, plan.strategy), mismatch)
339
+ if derived:
340
+ df = df.with_columns(derived)
341
+ if df.columns != plan.names:
342
+ df = df.select(
343
+ pl.col(n) if n in df.columns else pl.lit(None, pl.String).alias(n)
344
+ for n in plan.names
345
+ )
345
346
  columns = entity.schema_.columns
347
+ tag = "_e_cast_" # the mask columns' prefix: no declared column may start with it
348
+ while any(n.startswith(tag) for n in plan.names):
349
+ tag = f"_{tag}"
346
350
  try:
347
351
  casts = [
348
352
  cast_column(n, c.column_type, src.cast_mode, df.schema[n])
349
353
  for n, c in zip(plan.names, columns, strict=True)
350
354
  ]
351
- typed = df.with_columns(t for t, _ in casts)
352
- masks = df.with_columns(
353
- m for _, m in casts
354
- ) # unlike select, broadcasts constant masks
355
+ # one plan for both; unlike select, with_columns broadcasts the constant masks
356
+ masks = [m.alias(f"{tag}{n}") for n, (_, m) in zip(plan.names, casts, strict=True)] # fmt: skip
357
+ both = df.with_columns([t for t, _ in casts] + masks)
358
+ typed = both[plan.names]
355
359
  except pl.exceptions.InvalidOperationError as err:
356
360
  raise FloeRunError(f"entity.name={entity.name} cast failed: {err}") from err
357
361
  cast_checks = [
358
- RowCheck("cast_error", n, CAST_ERROR_MESSAGE, masks[f"_e_cast_{n}"])
362
+ RowCheck("cast_error", n, CAST_ERROR_MESSAGE, both[f"{tag}{n}"])
359
363
  for n, c in zip(plan.names, columns, strict=True)
360
364
  if c.column_type != "string"
361
365
  ] # fmt: skip
@@ -848,6 +852,11 @@ def _run_entity(
848
852
  entity = with_partition_columns(
849
853
  entity
850
854
  ) # D28: the derived columns are schema columns
855
+ merging = entity.sink.write_mode in ("merge_scd1", "merge_scd2")
856
+ configured = entity # the sink still checks a merge's key against the table
857
+ # #1091: a full refresh rebuilds a merge_scd1 table from every input in one commit
858
+ if inc.full_refresh and entity.sink.write_mode == "merge_scd1":
859
+ entity = entity.model_copy(update={"sink": entity.sink.model_copy(update={"write_mode": "overwrite"})}) # fmt: skip
851
860
  sink = entity.sink
852
861
  if sink.accepted.format not in ("parquet", "iceberg"):
853
862
  raise FloeRunError(f"entity.name={entity.name} unsupported sink format: {sink.accepted.format}") # fmt: skip
@@ -885,7 +894,7 @@ def _run_entity(
885
894
  if location and entity.incremental_mode in ("none", "archive"):
886
895
  plan.archive = location[0], storage_client(location[1])
887
896
  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:
897
+ if merging and entity.schema_.primary_key:
889
898
  pk = [runtime.get(k.strip(), k.strip()) for k in entity.schema_.primary_key]
890
899
  plan.merge_key = (
891
900
  pk[0] if len(pk) == 1 else f"[{','.join(pk)}]"
@@ -928,18 +937,25 @@ def _run_entity(
928
937
  skipped = len(files)
929
938
  downloader = None if isinstance(client, LocalClient) else client
930
939
  failure: list[Exception] = []
940
+ # #1090: a merge keeps a key's latest version, so the newest file is checked first; the first row of a file still wins.
941
+ # 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
942
+ latest_first = merging and entity.policy.severity != "abort"
931
943
 
932
944
  def accepted_frames() -> Generator[pl.DataFrame]:
933
945
  """Files are read as the sink pulls (#1056); `files` and `complete` are final once it is exhausted."""
934
946
  try:
935
- # Rust `localize_input`: a remote input is read from a local copy
936
- with tempfile.TemporaryDirectory(prefix="floe-") as temp_dir:
947
+ queue = pending[::-1] if latest_first else pending
948
+ # Rust `localize_input`: a remote input is read from a local copy; #1102: downloaded ahead, and closed before the temp dir goes
949
+ with (
950
+ tempfile.TemporaryDirectory(prefix="floe-") as temp_dir,
951
+ closing(localized_inputs(queue, downloader, Path(temp_dir))) as inputs,
952
+ ):
937
953
  if pending and sink.write_mode == "append" and plan.tracker.keys and (target is None or plan.table is not None): # fmt: skip
938
954
  plan.history = _key_cache(plan, accepted_client, base, Path(temp_dir)) # fmt: skip
939
- for f in pending:
955
+ for f in queue:
940
956
  emit("file_started", entity=entity.name, input=f.source_uri)
941
957
  timer = time.monotonic()
942
- local = localize_input(f, downloader, Path(temp_dir))
958
+ local = next(inputs)
943
959
  report, kept = _run_file(plan, f, local.local_path, Path(temp_dir))
944
960
  if local.is_ephemeral:
945
961
  local.local_path.unlink()
@@ -973,7 +989,7 @@ def _run_entity(
973
989
  write = write_accepted(frames, accepted_client, base, sink.write_mode, sink.accepted.options) # fmt: skip
974
990
  else:
975
991
  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
992
+ result, table, merge, added = write_iceberg(configured, target, frames, outputs, sink.write_mode, emit.run_id, lambda: input_hash(entity, complete)) # fmt: skip
977
993
  write = AcceptedWrite(result.files_written, result.file_paths, result.total_bytes_written,
978
994
  result.avg_file_size_mb, result.small_files_count) # fmt: skip
979
995
  if added:
@@ -989,11 +1005,15 @@ def _run_entity(
989
1005
  raise failure[0] from failure[0].__cause__
990
1006
  finally:
991
1007
  stream.close()
1008
+ if latest_first: # the report lists the files in file order
1009
+ files[skipped:] = files[skipped:][::-1]
992
1010
  files[skipped:] = sorted(files[skipped:], key=lambda r: r["input_file"] not in plan.prechecked) # fmt: skip
993
1011
  inc.committed = files # #915: from here on a failure promotes the written files
994
1012
  # #914: archive only once the sink committed, so a failed commit leaves the inputs in the source (Rust archives before)
995
1013
  by_uri = {r["input_file"]: r for r in files}
996
- for f in plan.to_archive:
1014
+ order = {f.source_uri: i for i, f in enumerate(inputs.files)}
1015
+ # #1090: oldest first, so a partial archive leaves the newest batch pending, never only an older one
1016
+ for f in sorted(plan.to_archive, key=lambda f: order[f.source_uri]) if latest_first else plan.to_archive: # fmt: skip
997
1017
  by_uri[f.source_uri]["output"]["archived_path"] = _archive(plan, f)
998
1018
  unique = [_report_names(r, key, runtime) for r, key in zip(plan.tracker.results(), keys, strict=True)] # fmt: skip
999
1019
  report = build_report(
@@ -0,0 +1,46 @@
1
+ """Download the inputs of one entity a few files ahead of the one being read (#1102)."""
2
+
3
+ from collections import deque
4
+ from collections.abc import Generator, Iterable
5
+ from concurrent.futures import Future, ThreadPoolExecutor
6
+ from pathlib import Path
7
+
8
+ from floe.storage.inputs import InputFile, LocalInputFile, localize_input
9
+ from floe.storage.types import Downloader
10
+
11
+ # ponytail: a fixed window of 8 files and 512 MiB (unknown size counts as 0; the next file is always allowed) and one pool per entity; a config knob or a run-wide pool once a bucket needs more
12
+ PREFETCH_WINDOW = 8
13
+ PREFETCH_BYTES = 512 * 1024 * 1024
14
+
15
+
16
+ def localized_inputs(
17
+ files: Iterable[InputFile], client: Downloader | None, temp_dir: Path
18
+ ) -> Generator[LocalInputFile]:
19
+ """`localize_input` for each file, in order. Close it to stop: pending downloads are cancelled and the running ones awaited."""
20
+ if client is None: # local inputs are used in place; no pool
21
+ for f in files:
22
+ yield localize_input(f, None, temp_dir)
23
+ return
24
+ todo = deque(files)
25
+ ahead: deque[tuple[Future[LocalInputFile], int]] = deque()
26
+ pool = ThreadPoolExecutor(PREFETCH_WINDOW, thread_name_prefix="floe-prefetch")
27
+
28
+ def refill() -> None:
29
+ held = sum(size for _, size in ahead)
30
+ while todo and len(ahead) < PREFETCH_WINDOW:
31
+ size = todo[0].source_size or 0
32
+ if ahead and held + size > PREFETCH_BYTES:
33
+ break
34
+ held += size
35
+ ahead.append(
36
+ (pool.submit(localize_input, todo.popleft(), client, temp_dir), size)
37
+ )
38
+
39
+ try:
40
+ refill()
41
+ while ahead:
42
+ local = ahead.popleft()[0].result()
43
+ refill()
44
+ yield local
45
+ finally:
46
+ pool.shutdown(cancel_futures=True)
floe/storage/resolve.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Storage reference resolution: name, else `storages.default`, else local (Rust `StorageResolver::resolve_path`)."""
2
2
 
3
3
  from pathlib import Path
4
+ from threading import Lock
4
5
 
5
6
  from floe.config.storage import StorageDefinition, StoragesConfig
6
7
  from floe.errors import FloeConfigError, FloeRunError
@@ -48,8 +49,23 @@ def resolve_path(
48
49
  return f"s3://{definition.bucket}/{key}" if key else f"s3://{definition.bucket}", definition # fmt: skip
49
50
 
50
51
 
52
+ _clients: dict[StorageDefinition, S3Client] = {}
53
+ _clients_lock = Lock()
54
+
55
+
56
+ def clear_storage_clients() -> None:
57
+ """Start of a run: the clients below read credentials and `AWS_*` env when built."""
58
+ with _clients_lock:
59
+ _clients.clear()
60
+
61
+
51
62
  def storage_client(definition: StorageDefinition | None) -> LocalClient | S3Client:
52
- """S3 credentials come from the environment, as in Rust."""
63
+ """S3 credentials come from the environment, as in Rust. One shared client per definition between `clear_storage_clients` calls."""
53
64
  if definition is None or definition.type == "local":
54
65
  return LocalClient()
55
- return S3Client(definition.bucket or "", definition.region, definition.endpoint, definition.path_style_access) # fmt: skip
66
+ with (
67
+ _clients_lock
68
+ ): # boto3 clients are thread-safe; the lock only stops a duplicate build
69
+ if definition not in _clients:
70
+ _clients[definition] = S3Client(definition.bucket or "", definition.region, definition.endpoint, definition.path_style_access) # fmt: skip
71
+ return _clients[definition]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: floe-python
3
- Version: 0.8.5
3
+ Version: 0.8.7
4
4
  Summary: Floe: a data-contract runtime for file ingestion
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,4 +1,4 @@
1
- floe/__init__.py,sha256=0X9pm8uWiwoDslKZyyjsEAYd6g1mlvIWhkWgE5uhGng,13279
1
+ floe/__init__.py,sha256=BMJil7rW2sI2OGGcEPUcz_mkZpyKe77sbdjGh7kjPAo,13362
2
2
  floe/__main__.py,sha256=Nzz6wrGXRpJiO59qlX7WEJRTlb-XcRFswmP0TA2KjyM,57
3
3
  floe/_cli.py,sha256=hwast3uMX0iUtkIpcChSIVHYkoYi4fR4jwCwQT53lXE,14101
4
4
  floe/_cli_output.py,sha256=VLxWQr-EPtIwklo9xr2CnjMD17U-7JrMUWdTtAP3jrY,8809
@@ -13,8 +13,8 @@ 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=4g-Mw-UOtLnitavD3lPWUUYweGqGMUx_8dhq2AJU8oc,4331
17
- floe/checks/rows.py,sha256=E-YUZUvBUyOB7zat2dtbPWFM-0T1-01lYKuQU1A6wto,4156
16
+ floe/checks/normalize.py,sha256=9fdRYFjWGXBAEw1Po71N5tsUWG8eekmSWIQktPduOig,4431
17
+ floe/checks/rows.py,sha256=-yPieSus0-7ZewLv2vR8waHqQ9DGMbVvmJh1_hK4eDs,4433
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
20
20
  floe/config/entity.py,sha256=CLXLXzkUbQhxenD_LwjBt0eoPreaYaxiBVEsdmnFIco,15883
@@ -27,17 +27,17 @@ floe/config/types.py,sha256=4eveoOlyGmNxyZpI1PSjli9lnXLPc1-jYj4fL8qHN_8,5038
27
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
- floe/iceberg/catalog.py,sha256=n2NlgqRgnTe3Etdyra70Ib2aYtpR7zXWrajKl7spyYg,15340
30
+ floe/iceberg/catalog.py,sha256=xjRCn6MFZCzleeM9DYkc8f6_In1qEQX_HQpDj1QSFWk,16380
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
37
37
  floe/lineage/observer.py,sha256=dDYfWvPpFGjdqvFqQDZ2pTjj7uDfqji2-BTID9hzg90,10075
38
38
  floe/read/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  floe/read/avro.py,sha256=rhGYlrWGFnqt_tyLIph57qV2TH91ULjoJ5qUlpW_6jI,1419
40
- floe/read/csv.py,sha256=5nXUGBa2s2Tt790UvOcrvfDym906JYjyhgunV6xPzr4,5153
40
+ floe/read/csv.py,sha256=1lNLh6MGrInWi3TADERA--rOG1iOVcgGdo7JPtt5LdI,5237
41
41
  floe/read/json.py,sha256=dPUXNlQUmMeYId3lef8ssEQeHi0tIOoEblvMQDmNVt4,8284
42
42
  floe/read/orc.py,sha256=TWgdlLBR2NF_oVZ5zEmRKNXoTkq5QfF2BOFzKyzSjhY,1553
43
43
  floe/read/parquet.py,sha256=JwfuWaHj2TxBKJeaKignq8p0Op_pUfsOdVh7dDC3wcE,1491
@@ -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=0QGSqgVTCcEH_Upp7A-Df376aN-pQc4G3F2N6uVebMw,8309
49
+ floe/run/entity.py,sha256=lSQYCAP-d-1O9R44wJF8kgO3U2B60d_PM37NmxLJJfs,49269
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
@@ -54,13 +54,14 @@ floe/storage/inputs.py,sha256=nmgEo2ub3hPUghLUXHU1wnKzpL4aYZbHb6S0XPYvMew,10410
54
54
  floe/storage/local.py,sha256=UiGFYEY33j45CQspWzRyJBW0HC2Nk9_zG--wL-aUuJI,6388
55
55
  floe/storage/partitioning.py,sha256=MG8RJDrwgZhucTswlXC2RwCmzrYIoOE6SH_aBM11KW8,6178
56
56
  floe/storage/paths.py,sha256=Z0fry3Gp0Tu2DpUeKqNXdqYP6n_KcKzK2llGx0TGHB8,2343
57
- floe/storage/resolve.py,sha256=pFAuVytTfnSEcTAVlJjFTj7S_Vipjry1ormWZP3Egmc,2593
57
+ floe/storage/prefetch.py,sha256=b-_be13xkJIw3LVndOb6SouTd1Pf8uZwG6Wy0Nk1pr8,1801
58
+ floe/storage/resolve.py,sha256=0hGSdU_p10j9vzET25Kcnd-QF6H7uN-hzpo_SC-DV_c,3154
58
59
  floe/storage/s3.py,sha256=XcG1qJTEY3KLSMLT950mUdrAxuCA8-Gx3fpNSzyV_Xo,7508
59
60
  floe/storage/types.py,sha256=FvAEXWMdVdopRK276JyETRYGOva2-uR8pZvsWj6Xvy0,993
60
61
  floe/write/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
62
  floe/write/outputs.py,sha256=cXxGUIJnZ38XPaqEI6a83GFUsapBRdY52XQm3GlUPcI,6937
62
- floe_python-0.8.5.dist-info/METADATA,sha256=qqbMLRbq0xRacIJKtPlBHp6_4L6jwcPIr2TeYYR-Kwg,442
63
- floe_python-0.8.5.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
64
- floe_python-0.8.5.dist-info/entry_points.txt,sha256=DU75r-E3zcK0quvwg5iT5DeWbUJck_Tz80bEj1IIzHA,40
65
- floe_python-0.8.5.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
66
- floe_python-0.8.5.dist-info/RECORD,,
63
+ floe_python-0.8.7.dist-info/METADATA,sha256=xk8WIhLuQqvT4lPVlzSISa5FRfsyhIpL3TjcfVUGGoo,442
64
+ floe_python-0.8.7.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
65
+ floe_python-0.8.7.dist-info/entry_points.txt,sha256=DU75r-E3zcK0quvwg5iT5DeWbUJck_Tz80bEj1IIzHA,40
66
+ floe_python-0.8.7.dist-info/licenses/LICENSE,sha256=dUhuoK-TCRQMpuLEAdfme-qPSJI0TlcH9jlNxeg9_EQ,1056
67
+ floe_python-0.8.7.dist-info/RECORD,,