floe-python 0.6.6__tar.gz → 0.6.7__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.
- {floe_python-0.6.6 → floe_python-0.6.7}/Cargo.lock +3 -3
- {floe_python-0.6.6 → floe_python-0.6.7}/PKG-INFO +1 -1
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/Cargo.toml +1 -1
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/lib.rs +12 -1
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/manifest/builder.rs +16 -4
- floe_python-0.6.7/crates/floe-core/src/manifest/mod.rs +8 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/manifest/model.rs +8 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/manifest/reconstruct.rs +22 -0
- floe_python-0.6.7/crates/floe-core/src/manifest/runtime.rs +139 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/mod.rs +35 -1
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/manifest/mod.rs +51 -8
- floe_python-0.6.7/crates/floe-core/tests/unit/manifest/runtime.rs +77 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/mod.rs +1 -0
- floe_python-0.6.7/crates/floe-core/tests/unit/run/replay_base.rs +41 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/Cargo.toml +2 -2
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/pyproject.duckdb.toml +2 -2
- {floe_python-0.6.6 → floe_python-0.6.7}/pyproject.toml +1 -1
- floe_python-0.6.6/crates/floe-core/src/manifest/mod.rs +0 -6
- {floe_python-0.6.6 → floe_python-0.6.7}/Cargo.toml +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/README.md +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/README.md +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/add_entity.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/checks/cast.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/checks/mismatch.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/checks/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/checks/normalize.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/checks/not_null.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/checks/unique.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/config/catalog.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/config/location.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/config/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/config/parse.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/config/storage.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/config/template.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/config/types.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/config/validate.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/config/yaml_decode.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/errors.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/format.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/avro.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/csv.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/fixed_width.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/json.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/json_selector.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/orc.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/parquet.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/xlsx.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/xml.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/read/xml_selector.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/core/extensions.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/core/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/core/paths.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/core/placement.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/core/planner.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/core/uri.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/core/validation.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/object_store.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/ops/archive.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/ops/inputs.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/ops/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/ops/output.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/providers/adls.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/providers/gcs.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/providers/local.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/providers/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/providers/s3.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/storage/target.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/unique_seed/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/accepted.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/arrow_convert.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/csv.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/delta/commit_metrics.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/delta/options.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/delta/record_batch.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/delta/unity.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/delta/unity_tests.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/delta.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/duckdb/conn.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/duckdb/merge.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/duckdb.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/iceberg/context.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/iceberg/data_files.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/iceberg/glue.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/iceberg/metadata.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/iceberg/rest.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/iceberg/schema.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/iceberg.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/metrics.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/parquet.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/parts.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/sink_format.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/strategy/append.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/strategy/merge/keys.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/strategy/merge/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/strategy/merge/scd1.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/strategy/merge/scd2.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/strategy/merge/shared.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/strategy/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/io/write/strategy/overwrite.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/lineage/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/log.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/profile/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/profile/parse.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/profile/types.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/profile/validate.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/report/build.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/report/entity.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/report/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/report/output.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/context.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/entity/accepted_buffer.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/entity/accepted_write.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/entity/incremental.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/entity/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/entity/pii.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/entity/precheck.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/entity/process.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/entity/resolve.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/entity/validate_split.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/events.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/file.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/output.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/run/perf.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/runner/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/runner/outcome.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/runtime.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/secret.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/state/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/vars/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/vars/resolve.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/src/warnings.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/archive_run.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/composite_unique.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/delta_run.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/dry_run.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/duckdb_run.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/fixed_width.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/iceberg_gcs_run.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/iceberg_glue_run.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/iceberg_run.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/iceberg_s3_run.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/json_selectors.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/local_run.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/path_normalization.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration/run_entities_filter.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/integration.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/common.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/add_entity.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/adls_storage.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/adls_validation.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/catalogs.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/config_validation.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/duckdb_validation.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/gcs_storage.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/gcs_validation.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/lineage_validation.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/local_storage.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/location.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/parse.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/pii_validation.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/remote_base.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/storage_resolver_uri.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/config/templating.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/format.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/avro_input.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/csv_nulls.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/json_array.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/json_ndjson.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/json_selector.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/orc_input.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/parquet_input.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/tsv.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/xlsx_input.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/xml.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/read/xml_selector.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/adls.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/adls_integration.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/gcs.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/inputs.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/local.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/paths.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/planner.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/s3.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/storage/target.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/write/delta_merge.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/write/delta_write.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/write/iceberg_write.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/write/metrics.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/write/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/write/object_store.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/write/parquet_write.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/write/parts.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/io/write/rejected_csv.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/profile/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/profile/parse.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/profile/validate.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/report/accepted_output.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/report/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/report/storage.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/check_order.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/checks.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/entity/accepted_output.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/entity/incremental.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/entity/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/lineage.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/normalize.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/pii.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/report.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/run/schema_mismatch.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/runner/adapter.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/runner/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/state/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/vars/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit/vars/resolve.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-core/tests/unit.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/.gitignore +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/README.md +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/src/functions.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/src/lib.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/src/observer.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/src/types/config.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/src/types/errors.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/src/types/mod.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/src/types/outcome.rs +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/tests/fixtures/config.yml +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/tests/fixtures/in/customer/customers_valid.csv +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/tests/fixtures/invalid_config.yml +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/tests/fixtures/profile.yml +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/crates/floe-python/tests/test_floe.py +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/python/floe/__init__.py +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/python/floe/_floe.pyi +0 -0
- {floe_python-0.6.6 → floe_python-0.6.7}/python/floe/py.typed +0 -0
|
@@ -3340,7 +3340,7 @@ dependencies = [
|
|
|
3340
3340
|
|
|
3341
3341
|
[[package]]
|
|
3342
3342
|
name = "floe-cli"
|
|
3343
|
-
version = "0.6.
|
|
3343
|
+
version = "0.6.7"
|
|
3344
3344
|
dependencies = [
|
|
3345
3345
|
"assert_cmd",
|
|
3346
3346
|
"clap",
|
|
@@ -3353,7 +3353,7 @@ dependencies = [
|
|
|
3353
3353
|
|
|
3354
3354
|
[[package]]
|
|
3355
3355
|
name = "floe-core"
|
|
3356
|
-
version = "0.6.
|
|
3356
|
+
version = "0.6.7"
|
|
3357
3357
|
dependencies = [
|
|
3358
3358
|
"apache-avro",
|
|
3359
3359
|
"arrow",
|
|
@@ -3399,7 +3399,7 @@ dependencies = [
|
|
|
3399
3399
|
|
|
3400
3400
|
[[package]]
|
|
3401
3401
|
name = "floe-python"
|
|
3402
|
-
version = "0.6.
|
|
3402
|
+
version = "0.6.7"
|
|
3403
3403
|
dependencies = [
|
|
3404
3404
|
"floe-core",
|
|
3405
3405
|
"pyo3",
|
|
@@ -38,7 +38,8 @@ pub use config::{
|
|
|
38
38
|
};
|
|
39
39
|
pub use errors::{FloeError, FloeErrorKind};
|
|
40
40
|
pub use manifest::{
|
|
41
|
-
build_common_manifest_json, config_from_manifest_json,
|
|
41
|
+
build_common_manifest_json, config_from_manifest_json, local_uri_for_env, manifest_runtime,
|
|
42
|
+
ManifestOptions, PathMode, RuntimeEnv, DEFAULT_WORK_ROOT,
|
|
42
43
|
};
|
|
43
44
|
pub use profile::{
|
|
44
45
|
detect_malformed_placeholder, detect_unresolved_placeholders, parse_profile,
|
|
@@ -172,3 +173,13 @@ pub fn extract_config_env_vars(
|
|
|
172
173
|
pub fn validate_config_for_tests(config: &config::RootConfig) -> FloeResult<()> {
|
|
173
174
|
config::validate_config(config)
|
|
174
175
|
}
|
|
176
|
+
|
|
177
|
+
/// Test-only accessor for the private manifest-replay path-resolution base (issue #443):
|
|
178
|
+
/// a manifest loaded from a remote URI must resolve local paths under its recorded
|
|
179
|
+
/// `work_root`, never against the manifest's own remote location.
|
|
180
|
+
pub fn manifest_replay_config_base_for_tests(
|
|
181
|
+
manifest_base: &config::ConfigBase,
|
|
182
|
+
json: &str,
|
|
183
|
+
) -> config::ConfigBase {
|
|
184
|
+
run::manifest_replay_config_base(manifest_base, json)
|
|
185
|
+
}
|
|
@@ -51,6 +51,10 @@ pub struct ManifestOptions {
|
|
|
51
51
|
pub default_domain: Option<String>,
|
|
52
52
|
/// Controls how the `path` field is set in source and sink entries.
|
|
53
53
|
pub path_mode: PathMode,
|
|
54
|
+
/// Runtime the manifest targets. `Image` records local `config_uri` / `profile_uri`
|
|
55
|
+
/// absolute under the container work-root (`/work`), restoring the portable pre-0.6.6
|
|
56
|
+
/// form; `Cli` keeps them relative / as-typed for cross-checkout reproducibility.
|
|
57
|
+
pub runtime_env: crate::manifest::RuntimeEnv,
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
#[derive(Debug)]
|
|
@@ -378,10 +382,16 @@ fn build_common_manifest(
|
|
|
378
382
|
});
|
|
379
383
|
}
|
|
380
384
|
|
|
381
|
-
//
|
|
382
|
-
// `
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
+
// A manifest is replayed by a different runner than the one that generated it, so
|
|
386
|
+
// `config_uri` must be absolute. `local_uri_for_env` absolutizes the local path against
|
|
387
|
+
// the target runtime's work-root: `Image` rebases the relative path under the container
|
|
388
|
+
// work-root (`local:///work/...`, portable + reproducible across containers), `Cli`
|
|
389
|
+
// records the host-canonical path. Remote config URIs pass through unchanged.
|
|
390
|
+
let config_uri = crate::manifest::local_uri_for_env(
|
|
391
|
+
&config_location.uri,
|
|
392
|
+
&config_location.path,
|
|
393
|
+
options.runtime_env,
|
|
394
|
+
);
|
|
385
395
|
let config_checksum = std::fs::read(&config_location.path)
|
|
386
396
|
.ok()
|
|
387
397
|
.map(|b| sha256_hex(&b));
|
|
@@ -423,6 +433,8 @@ fn build_common_manifest(
|
|
|
423
433
|
manifest_name: options.manifest_name.clone(),
|
|
424
434
|
manifest_id: build_manifest_id(&config_uri, config_checksum.as_deref()),
|
|
425
435
|
manifest_revision: None,
|
|
436
|
+
runtime_env: options.runtime_env.as_str(),
|
|
437
|
+
work_root: options.runtime_env.manifest_work_root(),
|
|
426
438
|
config_uri,
|
|
427
439
|
config_checksum,
|
|
428
440
|
profile_uri: options.profile_uri.clone(),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mod builder;
|
|
2
|
+
mod model;
|
|
3
|
+
pub mod reconstruct;
|
|
4
|
+
mod runtime;
|
|
5
|
+
|
|
6
|
+
pub use builder::{build_common_manifest_json, ManifestOptions, PathMode};
|
|
7
|
+
pub use reconstruct::{config_from_manifest_json, manifest_runtime};
|
|
8
|
+
pub use runtime::{local_uri_for_env, RuntimeEnv, DEFAULT_WORK_ROOT};
|
|
@@ -12,6 +12,14 @@ pub struct CommonManifest {
|
|
|
12
12
|
pub manifest_id: String,
|
|
13
13
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
14
14
|
pub manifest_revision: Option<String>,
|
|
15
|
+
/// Runtime the manifest targets: `"image"` (local URIs absolute under the container
|
|
16
|
+
/// work-root) or `"cli"` (local URIs relative / as-typed). Drives how a replaying
|
|
17
|
+
/// runner resolves local paths. See `RuntimeEnv`.
|
|
18
|
+
pub runtime_env: &'static str,
|
|
19
|
+
/// Work-root local paths are resolved under at replay time (e.g. `/work`). `Some` only
|
|
20
|
+
/// for `runtime_env == "image"`; `None` for `"cli"` (paths are relative, no fixed root).
|
|
21
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
22
|
+
pub work_root: Option<String>,
|
|
15
23
|
pub config_uri: String,
|
|
16
24
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
17
25
|
pub config_checksum: Option<String>,
|
|
@@ -110,6 +110,28 @@ fn deserialize_manifest_section<T: serde::de::DeserializeOwned>(
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/// The replay runtime hints recorded by the generator. Deserialized on their own (serde
|
|
114
|
+
/// ignores the rest of the manifest) so a runner can read them without depending on the full
|
|
115
|
+
/// `ManifestForRun` shape parsing cleanly.
|
|
116
|
+
#[derive(Debug, Default, Deserialize)]
|
|
117
|
+
struct ManifestRuntimeHints {
|
|
118
|
+
#[serde(default)]
|
|
119
|
+
runtime_env: Option<String>,
|
|
120
|
+
#[serde(default)]
|
|
121
|
+
work_root: Option<String>,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Parse the replay runtime hints (`runtime_env` / `work_root`) from a manifest JSON string.
|
|
125
|
+
/// Missing or malformed hints map to `(RuntimeEnv::Cli, None)`, so pre-0.6.7 manifests keep
|
|
126
|
+
/// their previous resolution behavior.
|
|
127
|
+
pub fn manifest_runtime(json: &str) -> (crate::manifest::RuntimeEnv, Option<String>) {
|
|
128
|
+
let hints: ManifestRuntimeHints = serde_json::from_str(json).unwrap_or_default();
|
|
129
|
+
(
|
|
130
|
+
crate::manifest::RuntimeEnv::from_manifest_str(hints.runtime_env.as_deref()),
|
|
131
|
+
hints.work_root,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
113
135
|
/// Parse a manifest JSON string and reconstruct a minimal RootConfig.
|
|
114
136
|
/// Returns (config, report_base_uri).
|
|
115
137
|
pub fn config_from_manifest_json(json: &str) -> FloeResult<(crate::config::RootConfig, String)> {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
|
|
3
|
+
/// Default container work-root. Matches `WORKDIR /work` in every Floe Dockerfile
|
|
4
|
+
/// (`Dockerfile`, `Dockerfile.duckdb`, `Dockerfile.release`).
|
|
5
|
+
pub const DEFAULT_WORK_ROOT: &str = "/work";
|
|
6
|
+
|
|
7
|
+
/// The runtime environment a generated manifest targets. Controls whether local
|
|
8
|
+
/// `config_uri` / `profile_uri` (and the `manifest_id` derived from them) are recorded
|
|
9
|
+
/// as absolute paths under a fixed container work-root, or kept relative / as-typed.
|
|
10
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
11
|
+
pub enum RuntimeEnv {
|
|
12
|
+
/// Container image: local paths are recorded absolute under the work-root (`/work`),
|
|
13
|
+
/// which is both portable for remote replay and reproducible across container runs
|
|
14
|
+
/// (every container uses the same `/work` mount).
|
|
15
|
+
Image,
|
|
16
|
+
/// Local CLI / dev checkout: local paths are recorded host-absolute (the canonicalized
|
|
17
|
+
/// path on the generating machine), so the manifest is resolvable on that host. It is
|
|
18
|
+
/// intentionally not reproducible across machines — the reproducible-CI case is served by
|
|
19
|
+
/// `Image` (fixed `/work`), not by CLI manifests.
|
|
20
|
+
#[default]
|
|
21
|
+
Cli,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl RuntimeEnv {
|
|
25
|
+
/// Detect the runtime from the environment when the CLI caller did not pass `--runtime`.
|
|
26
|
+
///
|
|
27
|
+
/// Precedence:
|
|
28
|
+
/// 1. `FLOE_RUNTIME=image|cli` — explicit override.
|
|
29
|
+
/// 2. Container heuristic: `/.dockerenv` exists, or the current dir is the `/work` mount.
|
|
30
|
+
/// 3. Otherwise `Cli`.
|
|
31
|
+
pub fn detect() -> Self {
|
|
32
|
+
if let Ok(value) = std::env::var("FLOE_RUNTIME") {
|
|
33
|
+
match value.trim().to_ascii_lowercase().as_str() {
|
|
34
|
+
"image" => return RuntimeEnv::Image,
|
|
35
|
+
"cli" => return RuntimeEnv::Cli,
|
|
36
|
+
_ => {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if Path::new("/.dockerenv").exists() {
|
|
40
|
+
return RuntimeEnv::Image;
|
|
41
|
+
}
|
|
42
|
+
if std::env::current_dir()
|
|
43
|
+
.ok()
|
|
44
|
+
.is_some_and(|dir| dir == Path::new(DEFAULT_WORK_ROOT))
|
|
45
|
+
{
|
|
46
|
+
return RuntimeEnv::Image;
|
|
47
|
+
}
|
|
48
|
+
RuntimeEnv::Cli
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// String recorded as `runtime_env` in the manifest.
|
|
52
|
+
pub fn as_str(self) -> &'static str {
|
|
53
|
+
match self {
|
|
54
|
+
RuntimeEnv::Image => "image",
|
|
55
|
+
RuntimeEnv::Cli => "cli",
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Parse the manifest-recorded `runtime_env` string. Unknown or missing maps to `Cli`,
|
|
60
|
+
/// which is backward-compatible: manifests generated before this field existed carry
|
|
61
|
+
/// relative local URIs and must keep resolving as before.
|
|
62
|
+
pub fn from_manifest_str(value: Option<&str>) -> Self {
|
|
63
|
+
match value {
|
|
64
|
+
Some(v) if v.eq_ignore_ascii_case("image") => RuntimeEnv::Image,
|
|
65
|
+
_ => RuntimeEnv::Cli,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Work-root that local paths are recorded relative to (at generation) and resolved
|
|
70
|
+
/// under (at replay). `Image` → `/work`, overridable via `FLOE_WORK_ROOT`;
|
|
71
|
+
/// `Cli` → the current working directory.
|
|
72
|
+
pub fn work_root(self) -> PathBuf {
|
|
73
|
+
match self {
|
|
74
|
+
RuntimeEnv::Image => std::env::var("FLOE_WORK_ROOT")
|
|
75
|
+
.ok()
|
|
76
|
+
.filter(|value| !value.trim().is_empty())
|
|
77
|
+
.map(PathBuf::from)
|
|
78
|
+
.unwrap_or_else(|| PathBuf::from(DEFAULT_WORK_ROOT)),
|
|
79
|
+
RuntimeEnv::Cli => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// The `work_root` value recorded in the manifest: `Some("/work")` for `Image`
|
|
84
|
+
/// (so remote replay resolves local paths against the same root), `None` for `Cli`
|
|
85
|
+
/// (paths are already host-absolute and self-contained, so there is no separate root
|
|
86
|
+
/// to record).
|
|
87
|
+
pub fn manifest_work_root(self) -> Option<String> {
|
|
88
|
+
match self {
|
|
89
|
+
RuntimeEnv::Image => Some(self.work_root().to_string_lossy().replace('\\', "/")),
|
|
90
|
+
RuntimeEnv::Cli => None,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Given the as-typed `local://…` URI recorded for a local config/profile (`uri`) and the
|
|
96
|
+
/// canonicalized absolute path of that file (`canonical_path`), return the absolute form
|
|
97
|
+
/// appropriate for `env`. Remote URIs are passed through unchanged.
|
|
98
|
+
///
|
|
99
|
+
/// Every local path in a manifest must be absolute, because the runner that generates the
|
|
100
|
+
/// manifest is not the one that replays it. The absolute base depends on the target runtime:
|
|
101
|
+
///
|
|
102
|
+
/// - `Image`: a *relative* local path is rebased under the container work-root
|
|
103
|
+
/// (`local://domains/x.yml` → `local:///work/domains/x.yml`) — portable for remote replay
|
|
104
|
+
/// and reproducible across containers (every container mounts `/work`).
|
|
105
|
+
/// - `Cli`: the path is recorded host-absolute from `canonical_path`
|
|
106
|
+
/// (`local:///Users/you/repo/domains/x.yml`) — resolvable on the generating host.
|
|
107
|
+
///
|
|
108
|
+
/// An already-absolute local path (leading `/`, or a Windows drive/UNC prefix) and any remote
|
|
109
|
+
/// URI are returned unchanged, in both modes.
|
|
110
|
+
pub fn local_uri_for_env(uri: &str, canonical_path: &Path, env: RuntimeEnv) -> String {
|
|
111
|
+
let Some(rel) = uri.strip_prefix("local://") else {
|
|
112
|
+
// Remote URIs (s3://, gs://, abfs://) are already environment-independent.
|
|
113
|
+
return uri.to_string();
|
|
114
|
+
};
|
|
115
|
+
match env {
|
|
116
|
+
RuntimeEnv::Cli => {
|
|
117
|
+
// Always record the canonicalized host-absolute path, regardless of how `-c` was
|
|
118
|
+
// typed (relative, or absolute with `..`). Resolvable on the generating host.
|
|
119
|
+
let abs = canonical_path.to_string_lossy().replace('\\', "/");
|
|
120
|
+
format!("local://{abs}")
|
|
121
|
+
}
|
|
122
|
+
RuntimeEnv::Image => {
|
|
123
|
+
if rel.starts_with('/') || has_drive_prefix(rel) {
|
|
124
|
+
// Already absolute — portable as-is; do not re-root under the work-root.
|
|
125
|
+
return uri.to_string();
|
|
126
|
+
}
|
|
127
|
+
let root = env.work_root();
|
|
128
|
+
let root = root.to_string_lossy().replace('\\', "/");
|
|
129
|
+
let root = root.trim_end_matches('/');
|
|
130
|
+
format!("local://{root}/{rel}")
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// True for a Windows drive prefix such as `C:` at the start of `path`.
|
|
136
|
+
fn has_drive_prefix(path: &str) -> bool {
|
|
137
|
+
let bytes = path.as_bytes();
|
|
138
|
+
bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
|
|
139
|
+
}
|
|
@@ -107,7 +107,7 @@ pub(crate) fn run_with_manifest_runtime(
|
|
|
107
107
|
let location = config::resolve_config_location(&manifest_str)?;
|
|
108
108
|
let json = std::fs::read_to_string(&location.path)?;
|
|
109
109
|
let (config, report_base_uri) = crate::manifest::config_from_manifest_json(&json)?;
|
|
110
|
-
let config_base = location.base
|
|
110
|
+
let config_base = manifest_replay_config_base(&location.base, &json);
|
|
111
111
|
if !options.entities.is_empty() {
|
|
112
112
|
validate_entities(&config, &options.entities)?;
|
|
113
113
|
}
|
|
@@ -121,6 +121,40 @@ pub(crate) fn run_with_manifest_runtime(
|
|
|
121
121
|
run_from_context(context, options, runtime)
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/// Choose the path-resolution base for manifest replay.
|
|
125
|
+
///
|
|
126
|
+
/// A manifest is self-contained: its source/sink URIs are already resolved and it embeds a
|
|
127
|
+
/// `storages` block. When it is loaded from a **remote** URI, the manifest's own bucket/prefix
|
|
128
|
+
/// must NOT become the resolution base — otherwise a relative path in the manifest (e.g. an
|
|
129
|
+
/// Iceberg sink that did not resolve to an absolute URI) is re-based onto the manifest bucket
|
|
130
|
+
/// and trips the `storage <name> bucket mismatch` guard (issue #443). Instead, resolve local
|
|
131
|
+
/// paths under the target runtime's work-root (manifest `work_root` → `FLOE_WORK_ROOT` →
|
|
132
|
+
/// `RuntimeEnv::detect()` default) and leave `remote_base` unset so remote paths resolve purely
|
|
133
|
+
/// from their storage definitions.
|
|
134
|
+
///
|
|
135
|
+
/// For a **local** manifest file, behavior is unchanged: relative paths resolve against the
|
|
136
|
+
/// manifest file's directory, exactly as before.
|
|
137
|
+
pub(crate) fn manifest_replay_config_base(
|
|
138
|
+
manifest_base: &config::ConfigBase,
|
|
139
|
+
json: &str,
|
|
140
|
+
) -> config::ConfigBase {
|
|
141
|
+
if manifest_base.remote_base().is_none() {
|
|
142
|
+
return manifest_base.clone();
|
|
143
|
+
}
|
|
144
|
+
let (runtime_env, manifest_work_root) = crate::manifest::manifest_runtime(json);
|
|
145
|
+
let work_root = manifest_work_root
|
|
146
|
+
.map(std::path::PathBuf::from)
|
|
147
|
+
.or_else(|| {
|
|
148
|
+
std::env::var("FLOE_WORK_ROOT")
|
|
149
|
+
.ok()
|
|
150
|
+
.filter(|value| !value.trim().is_empty())
|
|
151
|
+
.map(std::path::PathBuf::from)
|
|
152
|
+
})
|
|
153
|
+
.unwrap_or_else(|| runtime_env.work_root());
|
|
154
|
+
// `local_from_path` uses the parent as the local base dir, so join a sentinel file name.
|
|
155
|
+
config::ConfigBase::local_from_path(&work_root.join("manifest.json"))
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
pub fn run_with_runtime(
|
|
125
159
|
config_path: &Path,
|
|
126
160
|
config_base: config::ConfigBase,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
pub mod runtime;
|
|
2
|
+
|
|
1
3
|
use floe_core::{
|
|
2
4
|
build_common_manifest_json, config_from_manifest_json, load_config, parse_profile_from_str,
|
|
3
|
-
resolve_config_location, ManifestOptions, PathMode,
|
|
5
|
+
resolve_config_location, ManifestOptions, PathMode, RuntimeEnv,
|
|
4
6
|
};
|
|
5
7
|
// `validate_config_for_tests` is only exercised by the duckdb-gated MotherDuck
|
|
6
8
|
// round-trip assertion below; importing it unconditionally would warn (and fail the
|
|
@@ -44,10 +46,10 @@ fn manifest_uses_local_uri_for_local_config() {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
#[test]
|
|
47
|
-
fn
|
|
48
|
-
//
|
|
49
|
-
// bounce
|
|
50
|
-
// the
|
|
49
|
+
fn manifest_cli_config_uri_is_host_absolute_canonical() {
|
|
50
|
+
// Under `cli` runtime every local path is recorded host-absolute and canonicalized: a
|
|
51
|
+
// lexical `..` bounce in the typed path is collapsed, so config_uri is a clean absolute
|
|
52
|
+
// URI that resolves on the generating host (the definitive "all paths absolute" rule).
|
|
51
53
|
let temp_dir = tempfile::TempDir::new().expect("temp dir");
|
|
52
54
|
let cfg_dir = temp_dir.path().join("cfg");
|
|
53
55
|
std::fs::create_dir_all(&cfg_dir).expect("cfg dir");
|
|
@@ -58,7 +60,7 @@ fn manifest_config_uri_is_not_canonicalized() {
|
|
|
58
60
|
)
|
|
59
61
|
.expect("write config");
|
|
60
62
|
|
|
61
|
-
// <cfg_dir>/../cfg/config.yml —
|
|
63
|
+
// <cfg_dir>/../cfg/config.yml — cli canonicalization resolves this to
|
|
62
64
|
// <cfg_dir>/config.yml, dropping the "/../".
|
|
63
65
|
let bouncy = format!("{}/../cfg/config.yml", cfg_dir.display());
|
|
64
66
|
let loc = resolve_config_location(&bouncy).expect("resolve config location");
|
|
@@ -69,10 +71,51 @@ fn manifest_config_uri_is_not_canonicalized() {
|
|
|
69
71
|
|
|
70
72
|
let uri = value["config_uri"].as_str().expect("config_uri string");
|
|
71
73
|
assert!(
|
|
72
|
-
uri.
|
|
73
|
-
"config_uri
|
|
74
|
+
uri.starts_with("local:///"),
|
|
75
|
+
"cli config_uri must be host-absolute: {uri}"
|
|
76
|
+
);
|
|
77
|
+
assert!(
|
|
78
|
+
!uri.contains("/../"),
|
|
79
|
+
"cli config_uri must be canonicalized (no `..`): {uri}"
|
|
74
80
|
);
|
|
75
81
|
assert!(value["manifest_id"].as_str().unwrap().starts_with("mfv1-"));
|
|
82
|
+
// Default (no runtime override) auto-detects; in the test env that is `cli`.
|
|
83
|
+
assert_eq!(value["runtime_env"], "cli");
|
|
84
|
+
assert!(
|
|
85
|
+
value.get("work_root").is_none(),
|
|
86
|
+
"cli must not record work_root"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[test]
|
|
91
|
+
fn manifest_image_runtime_records_runtime_env_and_work_root() {
|
|
92
|
+
// `image` runtime records runtime_env/work_root so a remote runner knows how to resolve
|
|
93
|
+
// local paths. An absolute config path is already portable, so it is preserved unchanged
|
|
94
|
+
// (the relative -> /work re-rooting is covered by the runtime module's unit tests and the
|
|
95
|
+
// CLI `--runtime image` reproducibility test).
|
|
96
|
+
let temp_dir = tempfile::TempDir::new().expect("temp dir");
|
|
97
|
+
let cfg_dir = temp_dir.path().join("cfg");
|
|
98
|
+
std::fs::create_dir_all(&cfg_dir).expect("cfg dir");
|
|
99
|
+
let config_path = cfg_dir.join("config.yml");
|
|
100
|
+
std::fs::write(
|
|
101
|
+
&config_path,
|
|
102
|
+
"version: \"0.1\"\nentities:\n - name: orders\n source:\n format: csv\n path: ./in/orders\n sink:\n write_mode: overwrite\n accepted:\n format: parquet\n path: ./out/orders\n policy:\n severity: warn\n schema:\n columns:\n - name: id\n type: string\n",
|
|
103
|
+
)
|
|
104
|
+
.expect("write config");
|
|
105
|
+
|
|
106
|
+
let loc = resolve_config_location(config_path.to_str().expect("utf8")).expect("resolve");
|
|
107
|
+
let config = load_config(&loc.path).expect("load config");
|
|
108
|
+
let opts = ManifestOptions {
|
|
109
|
+
runtime_env: RuntimeEnv::Image,
|
|
110
|
+
..ManifestOptions::default()
|
|
111
|
+
};
|
|
112
|
+
let payload = build_common_manifest_json(&loc, &config, &[], None, &opts).expect("manifest");
|
|
113
|
+
let value: Value = serde_json::from_str(&payload).expect("valid json");
|
|
114
|
+
|
|
115
|
+
assert_eq!(value["runtime_env"], "image");
|
|
116
|
+
assert_eq!(value["work_root"], "/work");
|
|
117
|
+
let uri = value["config_uri"].as_str().expect("config_uri string");
|
|
118
|
+
assert!(uri.starts_with("local://"), "config_uri: {uri}");
|
|
76
119
|
}
|
|
77
120
|
|
|
78
121
|
#[test]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
use floe_core::{local_uri_for_env, RuntimeEnv};
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
#[test]
|
|
5
|
+
fn cli_records_host_absolute_canonical_path() {
|
|
6
|
+
// `Cli` ignores the (relative) as-typed URI and records the canonicalized
|
|
7
|
+
// host-absolute path, so the manifest is resolvable on the generating host.
|
|
8
|
+
assert_eq!(
|
|
9
|
+
local_uri_for_env(
|
|
10
|
+
"local://domains/orders.yml",
|
|
11
|
+
Path::new("/home/u/repo/domains/orders.yml"),
|
|
12
|
+
RuntimeEnv::Cli,
|
|
13
|
+
),
|
|
14
|
+
"local:///home/u/repo/domains/orders.yml"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[test]
|
|
19
|
+
fn image_absolutizes_relative_local_uri_under_work_root() {
|
|
20
|
+
// Default work-root is `/work` (no FLOE_WORK_ROOT set in the test env). `Image` uses
|
|
21
|
+
// the as-typed relative path and ignores the host-canonical path entirely.
|
|
22
|
+
assert_eq!(
|
|
23
|
+
local_uri_for_env(
|
|
24
|
+
"local://domains/orders.yml",
|
|
25
|
+
Path::new("/home/u/repo/domains/orders.yml"),
|
|
26
|
+
RuntimeEnv::Image,
|
|
27
|
+
),
|
|
28
|
+
"local:///work/domains/orders.yml"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[test]
|
|
33
|
+
fn already_absolute_local_uri_untouched_in_both_modes() {
|
|
34
|
+
for env in [RuntimeEnv::Cli, RuntimeEnv::Image] {
|
|
35
|
+
assert_eq!(
|
|
36
|
+
local_uri_for_env("local:///abs/orders.yml", Path::new("/abs/orders.yml"), env,),
|
|
37
|
+
"local:///abs/orders.yml"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[test]
|
|
43
|
+
fn remote_uris_pass_through_in_both_modes() {
|
|
44
|
+
for env in [RuntimeEnv::Cli, RuntimeEnv::Image] {
|
|
45
|
+
assert_eq!(
|
|
46
|
+
local_uri_for_env("s3://bucket/orders.yml", Path::new("/tmp/orders.yml"), env,),
|
|
47
|
+
"s3://bucket/orders.yml"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn from_manifest_str_maps_image_case_insensitively_else_cli() {
|
|
54
|
+
assert_eq!(
|
|
55
|
+
RuntimeEnv::from_manifest_str(Some("image")),
|
|
56
|
+
RuntimeEnv::Image
|
|
57
|
+
);
|
|
58
|
+
assert_eq!(
|
|
59
|
+
RuntimeEnv::from_manifest_str(Some("IMAGE")),
|
|
60
|
+
RuntimeEnv::Image
|
|
61
|
+
);
|
|
62
|
+
assert_eq!(RuntimeEnv::from_manifest_str(Some("cli")), RuntimeEnv::Cli);
|
|
63
|
+
assert_eq!(RuntimeEnv::from_manifest_str(None), RuntimeEnv::Cli);
|
|
64
|
+
assert_eq!(
|
|
65
|
+
RuntimeEnv::from_manifest_str(Some("weird")),
|
|
66
|
+
RuntimeEnv::Cli
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn manifest_work_root_recorded_only_for_image() {
|
|
72
|
+
assert_eq!(
|
|
73
|
+
RuntimeEnv::Image.manifest_work_root().as_deref(),
|
|
74
|
+
Some("/work")
|
|
75
|
+
);
|
|
76
|
+
assert_eq!(RuntimeEnv::Cli.manifest_work_root(), None);
|
|
77
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
use floe_core::config::ConfigBase;
|
|
2
|
+
use floe_core::manifest_replay_config_base_for_tests as manifest_replay_config_base;
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
4
|
+
|
|
5
|
+
#[test]
|
|
6
|
+
fn remote_manifest_replay_uses_recorded_work_root_not_manifest_bucket() {
|
|
7
|
+
// A manifest loaded from a remote URI must NOT resolve its paths against its own
|
|
8
|
+
// bucket (issue #443). With runtime_env=image/work_root=/work, the replay base is a
|
|
9
|
+
// local root at /work with no remote_base.
|
|
10
|
+
let remote = ConfigBase::remote_from_uri(
|
|
11
|
+
PathBuf::from("/tmp/manifest"),
|
|
12
|
+
"s3://openlakeforge-ops/floe/manifests/x.manifest.json",
|
|
13
|
+
)
|
|
14
|
+
.expect("remote base");
|
|
15
|
+
let json = r#"{"runtime_env":"image","work_root":"/work"}"#;
|
|
16
|
+
let base = manifest_replay_config_base(&remote, json);
|
|
17
|
+
assert!(
|
|
18
|
+
base.remote_base().is_none(),
|
|
19
|
+
"remote manifest bucket must not become the resolution base"
|
|
20
|
+
);
|
|
21
|
+
assert_eq!(base.local_dir(), Path::new("/work"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[test]
|
|
25
|
+
fn remote_manifest_replay_never_keeps_remote_base_even_without_hints() {
|
|
26
|
+
let remote = ConfigBase::remote_from_uri(
|
|
27
|
+
PathBuf::from("/tmp/manifest"),
|
|
28
|
+
"s3://bucket/x.manifest.json",
|
|
29
|
+
)
|
|
30
|
+
.expect("remote base");
|
|
31
|
+
let base = manifest_replay_config_base(&remote, "{}");
|
|
32
|
+
assert!(base.remote_base().is_none());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[test]
|
|
36
|
+
fn local_manifest_replay_base_is_unchanged() {
|
|
37
|
+
let local = ConfigBase::local_from_path(Path::new("/data/manifests/x.json"));
|
|
38
|
+
let base = manifest_replay_config_base(&local, "{}");
|
|
39
|
+
assert!(base.remote_base().is_none());
|
|
40
|
+
assert_eq!(base.local_dir(), Path::new("/data/manifests"));
|
|
41
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "floe-python"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.7"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Python bindings for floe-core (PyO3-based)"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -14,7 +14,7 @@ name = "_floe"
|
|
|
14
14
|
crate-type = ["cdylib"]
|
|
15
15
|
|
|
16
16
|
[dependencies]
|
|
17
|
-
floe-core = { path = "../floe-core", version = "0.6.
|
|
17
|
+
floe-core = { path = "../floe-core", version = "0.6.7", default-features = false }
|
|
18
18
|
pyo3 = { version = "0.29", features = ["extension-module", "abi3-py310"] }
|
|
19
19
|
serde_json = "1"
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ build-backend = "maturin"
|
|
|
22
22
|
|
|
23
23
|
[project]
|
|
24
24
|
name = "floe-duckdb"
|
|
25
|
-
version = "0.6.
|
|
25
|
+
version = "0.6.7"
|
|
26
26
|
description = "DuckDB companion extension for floe — adds the native DuckDB sink to the floe Python package"
|
|
27
27
|
readme = "README.md"
|
|
28
28
|
requires-python = ">=3.10"
|
|
@@ -46,7 +46,7 @@ classifiers = [
|
|
|
46
46
|
# only contributes the compiled DuckDB extension module into that package. Pin to
|
|
47
47
|
# the exact lean version so the companion's `_floe_duckdb` ABI always matches the
|
|
48
48
|
# `floe` package it plugs into.
|
|
49
|
-
dependencies = ["floe-python==0.6.
|
|
49
|
+
dependencies = ["floe-python==0.6.7"]
|
|
50
50
|
|
|
51
51
|
[project.urls]
|
|
52
52
|
Homepage = "https://github.com/malon64/floe"
|
|
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
|