floe-python 0.6.9__tar.gz → 0.6.11__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.9 → floe_python-0.6.11}/Cargo.lock +3 -3
- {floe_python-0.6.9 → floe_python-0.6.11}/PKG-INFO +1 -1
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/Cargo.toml +1 -1
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/lineage/mod.rs +100 -2
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/manifest/reconstruct.rs +69 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/lineage.rs +291 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/Cargo.toml +2 -2
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/pyproject.duckdb.toml +2 -2
- {floe_python-0.6.9 → floe_python-0.6.11}/pyproject.toml +1 -1
- {floe_python-0.6.9 → floe_python-0.6.11}/Cargo.toml +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/README.md +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/README.md +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/add_entity.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/checks/cast.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/checks/mismatch.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/checks/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/checks/normalize.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/checks/not_null.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/checks/unique.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/config/catalog.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/config/location.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/config/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/config/parse.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/config/storage.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/config/template.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/config/types.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/config/validate.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/config/yaml_decode.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/errors.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/format.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/avro.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/csv.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/fixed_width.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/json.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/json_selector.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/orc.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/parquet.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/xlsx.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/xml.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/read/xml_selector.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/core/extensions.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/core/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/core/paths.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/core/placement.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/core/planner.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/core/uri.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/core/validation.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/object_store.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/ops/archive.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/ops/inputs.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/ops/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/ops/output.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/providers/adls.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/providers/gcs.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/providers/local.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/providers/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/providers/s3.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/storage/target.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/unique_seed/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/accepted.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/arrow_convert.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/csv.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/delta/commit_metrics.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/delta/options.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/delta/record_batch.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/delta/unity.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/delta/unity_tests.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/delta.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/duckdb/conn.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/duckdb/merge.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/duckdb.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/iceberg/context.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/iceberg/data_files.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/iceberg/glue.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/iceberg/metadata.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/iceberg/rest.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/iceberg/schema.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/iceberg.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/metrics.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/parquet.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/parts.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/sink_format.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/strategy/append.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/strategy/merge/keys.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/strategy/merge/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/strategy/merge/scd1.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/strategy/merge/scd2.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/strategy/merge/shared.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/strategy/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/io/write/strategy/overwrite.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/lib.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/log.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/manifest/builder.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/manifest/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/manifest/model.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/manifest/runtime.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/profile/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/profile/parse.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/profile/types.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/profile/validate.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/report/build.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/report/entity.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/report/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/report/output.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/context.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/entity/accepted_buffer.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/entity/accepted_write.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/entity/incremental.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/entity/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/entity/pii.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/entity/precheck.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/entity/process.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/entity/resolve.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/entity/validate_split.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/events.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/file.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/output.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/run/perf.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/runner/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/runner/outcome.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/runtime.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/secret.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/state/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/vars/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/vars/resolve.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/src/warnings.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/archive_run.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/composite_unique.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/delta_run.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/dry_run.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/duckdb_run.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/fixed_width.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/iceberg_gcs_run.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/iceberg_glue_run.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/iceberg_run.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/iceberg_s3_run.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/json_selectors.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/local_run.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/path_normalization.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration/run_entities_filter.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/integration.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/common.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/add_entity.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/adls_storage.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/adls_validation.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/catalogs.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/config_validation.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/duckdb_validation.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/gcs_storage.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/gcs_validation.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/lineage_validation.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/local_storage.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/location.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/parse.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/pii_validation.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/remote_base.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/storage_resolver_uri.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/config/templating.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/format.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/avro_input.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/csv_nulls.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/json_array.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/json_ndjson.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/json_selector.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/orc_input.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/parquet_input.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/tsv.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/xlsx_input.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/xml.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/read/xml_selector.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/adls.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/adls_integration.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/gcs.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/inputs.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/local.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/paths.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/planner.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/s3.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/storage/target.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/write/delta_merge.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/write/delta_write.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/write/iceberg_write.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/write/metrics.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/write/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/write/object_store.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/write/parquet_write.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/write/parts.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/io/write/rejected_csv.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/manifest/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/manifest/runtime.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/profile/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/profile/parse.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/profile/validate.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/report/accepted_output.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/report/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/report/storage.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/check_order.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/checks.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/entity/accepted_output.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/entity/incremental.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/entity/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/normalize.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/pii.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/replay_base.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/report.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/run/schema_mismatch.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/runner/adapter.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/runner/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/state/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/vars/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit/vars/resolve.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-core/tests/unit.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/.gitignore +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/README.md +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/src/functions.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/src/lib.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/src/observer.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/src/types/config.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/src/types/errors.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/src/types/mod.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/src/types/outcome.rs +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/tests/fixtures/config.yml +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/tests/fixtures/in/customer/customers_valid.csv +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/tests/fixtures/invalid_config.yml +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/tests/fixtures/profile.yml +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/crates/floe-python/tests/test_floe.py +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/python/floe/__init__.py +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/python/floe/_floe.pyi +0 -0
- {floe_python-0.6.9 → floe_python-0.6.11}/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.11"
|
|
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.11"
|
|
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.11"
|
|
3403
3403
|
dependencies = [
|
|
3404
3404
|
"floe-core",
|
|
3405
3405
|
"pyo3",
|
|
@@ -8,12 +8,15 @@ use uuid::Uuid;
|
|
|
8
8
|
|
|
9
9
|
use crate::config::{EntityConfig, LineageConfig};
|
|
10
10
|
use crate::run::events::{RunEvent, RunObserver};
|
|
11
|
+
use crate::secret::Secret;
|
|
11
12
|
|
|
12
13
|
const DEFAULT_PRODUCER: &str = concat!(
|
|
13
14
|
"https://github.com/malon64/floe/releases/tag/v",
|
|
14
15
|
env!("CARGO_PKG_VERSION")
|
|
15
16
|
);
|
|
16
17
|
|
|
18
|
+
const OPENLINEAGE_API_KEY_ENV: &str = "OPENLINEAGE_API_KEY";
|
|
19
|
+
|
|
17
20
|
#[derive(Clone)]
|
|
18
21
|
struct ColumnMapping {
|
|
19
22
|
output_name: String,
|
|
@@ -44,9 +47,36 @@ fn resolve_lineage_url(base: &str, endpoint: Option<&str>) -> String {
|
|
|
44
47
|
)
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
fn is_unresolved_placeholder(value: &str) -> bool {
|
|
51
|
+
let value = value.trim();
|
|
52
|
+
value.starts_with("{{") && value.ends_with("}}")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Effective Bearer token for lineage POSTs.
|
|
56
|
+
///
|
|
57
|
+
/// An explicit `lineage.api_key` wins for backward compatibility. Otherwise Floe falls back
|
|
58
|
+
/// to the ambient `OPENLINEAGE_API_KEY`, mirroring the OpenLineage Python client so a replayed
|
|
59
|
+
/// manifest authenticates from a runner-mounted Secret without the credential ever being
|
|
60
|
+
/// persisted in the manifest. An unexpanded `{{...}}` placeholder is treated as absent:
|
|
61
|
+
/// profile-supplied lineage is merged after config templating (see `apply_profile_lineage`),
|
|
62
|
+
/// so the raw placeholder would otherwise be sent verbatim as the token.
|
|
63
|
+
fn resolve_api_key(configured: Option<&str>, ambient: Option<String>) -> Option<Secret> {
|
|
64
|
+
let explicit = configured
|
|
65
|
+
.map(str::trim)
|
|
66
|
+
.filter(|key| !key.is_empty() && !is_unresolved_placeholder(key));
|
|
67
|
+
match explicit {
|
|
68
|
+
Some(key) => Some(Secret::from(key)),
|
|
69
|
+
None => ambient
|
|
70
|
+
.map(|value| value.trim().to_string())
|
|
71
|
+
.filter(|value| !value.is_empty())
|
|
72
|
+
.map(Secret::from),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
pub struct OpenLineageObserver {
|
|
48
77
|
client: reqwest::blocking::Client,
|
|
49
78
|
config: LineageConfig,
|
|
79
|
+
api_key: Option<Secret>,
|
|
50
80
|
entity_start_ms: Mutex<HashMap<String, u128>>,
|
|
51
81
|
entity_run_ids: Mutex<HashMap<String, String>>,
|
|
52
82
|
run_start_ms: Mutex<Option<u128>>,
|
|
@@ -155,9 +185,15 @@ impl OpenLineageObserver {
|
|
|
155
185
|
})
|
|
156
186
|
.collect();
|
|
157
187
|
|
|
188
|
+
let api_key = resolve_api_key(
|
|
189
|
+
config.api_key.as_deref(),
|
|
190
|
+
std::env::var(OPENLINEAGE_API_KEY_ENV).ok(),
|
|
191
|
+
);
|
|
192
|
+
|
|
158
193
|
Ok(Self {
|
|
159
194
|
client,
|
|
160
195
|
config: config.clone(),
|
|
196
|
+
api_key,
|
|
161
197
|
entity_start_ms: Mutex::new(HashMap::new()),
|
|
162
198
|
entity_run_ids: Mutex::new(HashMap::new()),
|
|
163
199
|
run_start_ms: Mutex::new(None),
|
|
@@ -172,8 +208,8 @@ impl OpenLineageObserver {
|
|
|
172
208
|
|
|
173
209
|
fn attempt_post(&self, url: &str, body: &Value) -> Result<(), bool> {
|
|
174
210
|
let mut req = self.client.post(url).json(body);
|
|
175
|
-
if let Some(api_key) = self.
|
|
176
|
-
req = req.bearer_auth(api_key);
|
|
211
|
+
if let Some(api_key) = self.api_key.as_ref() {
|
|
212
|
+
req = req.bearer_auth(api_key.expose());
|
|
177
213
|
}
|
|
178
214
|
match req.send() {
|
|
179
215
|
Err(_) => Err(true),
|
|
@@ -599,6 +635,23 @@ pub fn build_observer(
|
|
|
599
635
|
Ok(Arc::new(obs))
|
|
600
636
|
}
|
|
601
637
|
|
|
638
|
+
/// Build a lineage observer for a `--manifest` replay from the manifest's
|
|
639
|
+
/// embedded lineage block and its entities' resolved cloud datasets, so replay
|
|
640
|
+
/// emits the same input/output lineage as a direct config run. `Ok(None)` when
|
|
641
|
+
/// the manifest has no `lineage` block. See
|
|
642
|
+
/// [`crate::manifest::reconstruct::lineage_inputs_from_manifest_json`].
|
|
643
|
+
pub fn build_observer_from_manifest_json(
|
|
644
|
+
manifest_json: &str,
|
|
645
|
+
config_path: &str,
|
|
646
|
+
) -> crate::FloeResult<Option<Arc<dyn RunObserver>>> {
|
|
647
|
+
match crate::manifest::reconstruct::lineage_inputs_from_manifest_json(manifest_json)? {
|
|
648
|
+
Some((lineage_cfg, entities)) => {
|
|
649
|
+
build_observer(&lineage_cfg, &entities, config_path).map(Some)
|
|
650
|
+
}
|
|
651
|
+
None => Ok(None),
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
602
655
|
impl OpenLineageObserver {
|
|
603
656
|
pub fn is_circuit_open(&self) -> bool {
|
|
604
657
|
self.circuit_open.load(Ordering::Relaxed)
|
|
@@ -608,3 +661,48 @@ impl OpenLineageObserver {
|
|
|
608
661
|
self.consecutive_failures.load(Ordering::Relaxed)
|
|
609
662
|
}
|
|
610
663
|
}
|
|
664
|
+
|
|
665
|
+
#[cfg(test)]
|
|
666
|
+
mod tests {
|
|
667
|
+
use super::*;
|
|
668
|
+
|
|
669
|
+
fn expose(key: Option<Secret>) -> Option<String> {
|
|
670
|
+
key.map(|s| s.expose().to_string())
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
#[test]
|
|
674
|
+
fn explicit_config_key_wins_over_env() {
|
|
675
|
+
let key = resolve_api_key(Some("cfg-token"), Some("env-token".to_string()));
|
|
676
|
+
assert_eq!(expose(key), Some("cfg-token".to_string()));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
#[test]
|
|
680
|
+
fn falls_back_to_env_when_config_absent() {
|
|
681
|
+
let key = resolve_api_key(None, Some("env-token".to_string()));
|
|
682
|
+
assert_eq!(expose(key), Some("env-token".to_string()));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
#[test]
|
|
686
|
+
fn unresolved_placeholder_falls_back_to_env() {
|
|
687
|
+
let key = resolve_api_key(
|
|
688
|
+
Some("{{OPENLINEAGE_API_KEY}}"),
|
|
689
|
+
Some("env-token".to_string()),
|
|
690
|
+
);
|
|
691
|
+
assert_eq!(expose(key), Some("env-token".to_string()));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
#[test]
|
|
695
|
+
fn unresolved_placeholder_without_env_is_none() {
|
|
696
|
+
assert!(resolve_api_key(Some("{{OPENLINEAGE_API_KEY}}"), None).is_none());
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
#[test]
|
|
700
|
+
fn none_when_neither_source_present() {
|
|
701
|
+
assert!(resolve_api_key(None, None).is_none());
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
#[test]
|
|
705
|
+
fn blank_values_are_ignored() {
|
|
706
|
+
assert!(resolve_api_key(Some(" "), Some(" ".to_string())).is_none());
|
|
707
|
+
}
|
|
708
|
+
}
|
|
@@ -56,6 +56,10 @@ pub struct ManifestSinksForRun {
|
|
|
56
56
|
pub struct ManifestSinkTargetForRun {
|
|
57
57
|
pub format: String,
|
|
58
58
|
pub storage: String,
|
|
59
|
+
/// Resolved cloud URI (e.g. `s3://bucket/key`). Optional so hand-authored
|
|
60
|
+
/// manifests that omit it still parse; the generator always writes it.
|
|
61
|
+
#[serde(default)]
|
|
62
|
+
pub uri: Option<String>,
|
|
59
63
|
pub path: String,
|
|
60
64
|
pub options: Option<serde_json::Value>,
|
|
61
65
|
pub partition_by: Option<Vec<String>>,
|
|
@@ -172,6 +176,71 @@ pub fn config_from_manifest_json(json: &str) -> FloeResult<(crate::config::RootC
|
|
|
172
176
|
Ok((config, manifest.report_base_uri))
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
/// Reconstruct the lineage config and entities for a `--manifest` replay's
|
|
180
|
+
/// OpenLineage observer, with each source/sink dataset path resolved to the
|
|
181
|
+
/// manifest's cloud `uri`.
|
|
182
|
+
///
|
|
183
|
+
/// The run reconstruction (`config_from_manifest_json`) intentionally keeps the
|
|
184
|
+
/// raw `path` fields — in the default manifest path mode those hold the original
|
|
185
|
+
/// config paths (e.g. `sales/orders`) while the resolved cloud identity lives in
|
|
186
|
+
/// `uri` (e.g. `s3://bucket/sales/orders`). The observer derives a dataset's
|
|
187
|
+
/// namespace/name from the path, so for lineage it must see the resolved cloud
|
|
188
|
+
/// URI; otherwise a named S3/GCS/ABFS source is reported under the `file`
|
|
189
|
+
/// namespace. Local and relative paths are left as reconstructed.
|
|
190
|
+
///
|
|
191
|
+
/// Returns `Ok(None)` when the manifest has no `lineage` block.
|
|
192
|
+
pub fn lineage_inputs_from_manifest_json(
|
|
193
|
+
json: &str,
|
|
194
|
+
) -> FloeResult<Option<(LineageConfig, Vec<EntityConfig>)>> {
|
|
195
|
+
let manifest: ManifestForRun =
|
|
196
|
+
serde_json::from_str(json).map_err(|err| -> Box<dyn std::error::Error + Send + Sync> {
|
|
197
|
+
FloeError::config(format!("manifest parse error: {err}")).into()
|
|
198
|
+
})?;
|
|
199
|
+
|
|
200
|
+
let lineage = match deserialize_manifest_section::<LineageConfig>(
|
|
201
|
+
manifest.lineage.as_ref(),
|
|
202
|
+
"lineage",
|
|
203
|
+
)? {
|
|
204
|
+
Some(lineage) => lineage,
|
|
205
|
+
None => return Ok(None),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let mut entities = manifest
|
|
209
|
+
.entities
|
|
210
|
+
.iter()
|
|
211
|
+
.map(entity_from_manifest)
|
|
212
|
+
.collect::<FloeResult<Vec<_>>>()?;
|
|
213
|
+
|
|
214
|
+
// Reconstructed entities are in manifest order, so zip pairs each with its
|
|
215
|
+
// source manifest entry to overlay the resolved cloud URIs.
|
|
216
|
+
for (entity, m) in entities.iter_mut().zip(manifest.entities.iter()) {
|
|
217
|
+
overlay_cloud_uri(&mut entity.source.path, Some(m.source.uri.as_str()));
|
|
218
|
+
overlay_cloud_uri(
|
|
219
|
+
&mut entity.sink.accepted.path,
|
|
220
|
+
m.sinks.accepted.uri.as_deref(),
|
|
221
|
+
);
|
|
222
|
+
if let (Some(rejected), Some(m_rejected)) =
|
|
223
|
+
(entity.sink.rejected.as_mut(), m.sinks.rejected.as_ref())
|
|
224
|
+
{
|
|
225
|
+
overlay_cloud_uri(&mut rejected.path, m_rejected.uri.as_deref());
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
Ok(Some((lineage, entities)))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Replace `path` with `uri` when `uri` names a cloud object store; other
|
|
233
|
+
/// schemes (local / relative) keep the reconstructed path. The prefix list
|
|
234
|
+
/// mirrors `lineage::split_storage_uri` and must stay in sync with it.
|
|
235
|
+
fn overlay_cloud_uri(path: &mut String, uri: Option<&str>) {
|
|
236
|
+
const CLOUD_SCHEMES: [&str; 6] = ["s3://", "gs://", "gcs://", "az://", "abfss://", "abfs://"];
|
|
237
|
+
if let Some(uri) = uri {
|
|
238
|
+
if CLOUD_SCHEMES.iter().any(|scheme| uri.starts_with(scheme)) {
|
|
239
|
+
*path = uri.to_string();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
175
244
|
fn entity_from_manifest(m: &ManifestEntityForRun) -> FloeResult<EntityConfig> {
|
|
176
245
|
let policy_severity = parse_policy_severity(m.policy_severity.as_deref().unwrap_or("warn"));
|
|
177
246
|
let write_mode = parse_write_mode(m.write_mode.as_deref().unwrap_or("overwrite"));
|
|
@@ -1092,3 +1092,294 @@ fn iceberg_accepted_dataset_namespace_defaults_to_job_namespace() {
|
|
|
1092
1092
|
_start_mock.assert();
|
|
1093
1093
|
_complete_mock.assert();
|
|
1094
1094
|
}
|
|
1095
|
+
|
|
1096
|
+
// Sole owner of the process-wide OPENLINEAGE_API_KEY variable. The crate has no
|
|
1097
|
+
// serial-test harness, so all three auth paths are asserted inside one test that
|
|
1098
|
+
// controls the variable's lifecycle; no other test reads it, and every other lineage
|
|
1099
|
+
// test either sets an explicit api_key (which wins over the env regardless) or does not
|
|
1100
|
+
// match on request headers, so this cannot race them.
|
|
1101
|
+
#[test]
|
|
1102
|
+
fn api_key_resolution_over_the_wire() {
|
|
1103
|
+
std::env::remove_var("OPENLINEAGE_API_KEY");
|
|
1104
|
+
|
|
1105
|
+
// No config api_key and no env var => request carries no Authorization header.
|
|
1106
|
+
{
|
|
1107
|
+
let mut server = mockito::Server::new();
|
|
1108
|
+
let mock = server
|
|
1109
|
+
.mock("POST", "/api/v1/lineage")
|
|
1110
|
+
.match_header("authorization", mockito::Matcher::Missing)
|
|
1111
|
+
.with_status(200)
|
|
1112
|
+
.expect(1)
|
|
1113
|
+
.create();
|
|
1114
|
+
let config = make_config(&server.url(), None);
|
|
1115
|
+
let obs = OpenLineageObserver::new(&config, &[], "config.yml").unwrap();
|
|
1116
|
+
obs.on_event(run_started_event());
|
|
1117
|
+
mock.assert();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Manifest replay with api_key omitted + env var set => Bearer token from the env.
|
|
1121
|
+
{
|
|
1122
|
+
let mut server = mockito::Server::new();
|
|
1123
|
+
let mock = server
|
|
1124
|
+
.mock("POST", "/api/v1/lineage")
|
|
1125
|
+
.match_header("authorization", "Bearer env-replay-token")
|
|
1126
|
+
.with_status(200)
|
|
1127
|
+
.expect(1)
|
|
1128
|
+
.create();
|
|
1129
|
+
let manifest = format!(
|
|
1130
|
+
r#"{{"spec_version":"0.3","report_base_uri":"file:///tmp","entities":[],"lineage":{{"url":"{}","namespace":"test-ns"}}}}"#,
|
|
1131
|
+
server.url()
|
|
1132
|
+
);
|
|
1133
|
+
std::env::set_var("OPENLINEAGE_API_KEY", "env-replay-token");
|
|
1134
|
+
let (config, _) =
|
|
1135
|
+
floe_core::config_from_manifest_json(&manifest).expect("reconstruct manifest config");
|
|
1136
|
+
let lineage = config.lineage.expect("manifest lineage block");
|
|
1137
|
+
let obs = OpenLineageObserver::new(&lineage, &[], "").unwrap();
|
|
1138
|
+
obs.on_event(run_started_event());
|
|
1139
|
+
mock.assert();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Explicit config api_key wins even when the env var is present (backward compatible).
|
|
1143
|
+
{
|
|
1144
|
+
let mut server = mockito::Server::new();
|
|
1145
|
+
let mock = server
|
|
1146
|
+
.mock("POST", "/api/v1/lineage")
|
|
1147
|
+
.match_header("authorization", "Bearer cfg-token")
|
|
1148
|
+
.with_status(200)
|
|
1149
|
+
.expect(1)
|
|
1150
|
+
.create();
|
|
1151
|
+
let mut config = make_config(&server.url(), None);
|
|
1152
|
+
config.api_key = Some("cfg-token".to_string());
|
|
1153
|
+
let obs = OpenLineageObserver::new(&config, &[], "config.yml").unwrap();
|
|
1154
|
+
obs.on_event(run_started_event());
|
|
1155
|
+
mock.assert();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
std::env::remove_var("OPENLINEAGE_API_KEY");
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// ---- Manifest-replay dataset lineage (issue #455) ----
|
|
1162
|
+
// build_observer_from_manifest_json must feed the manifest's entities and their
|
|
1163
|
+
// resolved cloud URIs to the observer so replay emits non-empty inputs/outputs.
|
|
1164
|
+
|
|
1165
|
+
fn replay_entity(name: &str, source_uri: &str, accepted_uri: &str) -> serde_json::Value {
|
|
1166
|
+
json!({
|
|
1167
|
+
"name": name,
|
|
1168
|
+
"source": { "format": "csv", "storage": "bronze", "uri": source_uri, "path": source_uri },
|
|
1169
|
+
"sinks": {
|
|
1170
|
+
"accepted": {
|
|
1171
|
+
"format": "parquet",
|
|
1172
|
+
"storage": "warehouse",
|
|
1173
|
+
"uri": accepted_uri,
|
|
1174
|
+
"path": accepted_uri
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
"schema": { "columns": [], "primary_key": [], "unique_keys": [] }
|
|
1178
|
+
})
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
fn replay_manifest(server_url: &str, entities: serde_json::Value) -> String {
|
|
1182
|
+
json!({
|
|
1183
|
+
"spec_version": "0.3",
|
|
1184
|
+
"report_base_uri": "file:///tmp",
|
|
1185
|
+
"entities": entities,
|
|
1186
|
+
"lineage": { "url": server_url, "namespace": "test-ns" }
|
|
1187
|
+
})
|
|
1188
|
+
.to_string()
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
fn entity_started(name: &str) -> RunEvent {
|
|
1192
|
+
RunEvent::EntityStarted {
|
|
1193
|
+
run_id: "test-run-1".to_string(),
|
|
1194
|
+
name: name.to_string(),
|
|
1195
|
+
ts_ms: 1_001_000,
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Replaying a manifest with an entity emits the entity's source input and
|
|
1200
|
+
// accepted output — the regression from issue #455. With entities dropped
|
|
1201
|
+
// (the previous `&[]`), inputs/outputs would be empty and the subset match on
|
|
1202
|
+
// inputs[0]/outputs[0] would fail.
|
|
1203
|
+
#[test]
|
|
1204
|
+
fn manifest_replay_passes_entities_to_observer() {
|
|
1205
|
+
let mut server = mockito::Server::new();
|
|
1206
|
+
|
|
1207
|
+
let start_mock = server
|
|
1208
|
+
.mock("POST", "/api/v1/lineage")
|
|
1209
|
+
.match_body(mockito::Matcher::PartialJson(
|
|
1210
|
+
json!({ "eventType": "START" }),
|
|
1211
|
+
))
|
|
1212
|
+
.with_status(200)
|
|
1213
|
+
.expect(1)
|
|
1214
|
+
.create();
|
|
1215
|
+
|
|
1216
|
+
let complete_mock = server
|
|
1217
|
+
.mock("POST", "/api/v1/lineage")
|
|
1218
|
+
.match_body(mockito::Matcher::PartialJson(json!({
|
|
1219
|
+
"eventType": "COMPLETE",
|
|
1220
|
+
"inputs": [{
|
|
1221
|
+
"namespace": "s3://lakehouse-bronze",
|
|
1222
|
+
"name": "sales/customer_health/support_tickets"
|
|
1223
|
+
}],
|
|
1224
|
+
"outputs": [{
|
|
1225
|
+
"namespace": "s3://lakehouse-warehouse",
|
|
1226
|
+
"name": "silver/support_tickets"
|
|
1227
|
+
}]
|
|
1228
|
+
})))
|
|
1229
|
+
.with_status(200)
|
|
1230
|
+
.expect(1)
|
|
1231
|
+
.create();
|
|
1232
|
+
|
|
1233
|
+
let manifest = replay_manifest(
|
|
1234
|
+
&server.url(),
|
|
1235
|
+
json!([replay_entity(
|
|
1236
|
+
"support_tickets",
|
|
1237
|
+
"s3://lakehouse-bronze/sales/customer_health/support_tickets",
|
|
1238
|
+
"s3://lakehouse-warehouse/silver/support_tickets",
|
|
1239
|
+
)]),
|
|
1240
|
+
);
|
|
1241
|
+
|
|
1242
|
+
let obs = floe_core::lineage::build_observer_from_manifest_json(&manifest, "")
|
|
1243
|
+
.expect("observer builds from manifest")
|
|
1244
|
+
.expect("manifest has a lineage block => Some observer");
|
|
1245
|
+
|
|
1246
|
+
obs.on_event(entity_started("support_tickets"));
|
|
1247
|
+
obs.on_event(entity_finished_event("support_tickets", "success"));
|
|
1248
|
+
|
|
1249
|
+
start_mock.assert();
|
|
1250
|
+
complete_mock.assert();
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Default manifest path mode records raw config paths in `path` and the resolved
|
|
1254
|
+
// cloud identity in `uri`. Lineage must report the cloud dataset from `uri`, not
|
|
1255
|
+
// a `file` namespace derived from the raw path (PR #456 review).
|
|
1256
|
+
#[test]
|
|
1257
|
+
fn manifest_replay_default_path_mode_uses_resolved_cloud_uri() {
|
|
1258
|
+
let mut server = mockito::Server::new();
|
|
1259
|
+
|
|
1260
|
+
let start_mock = server
|
|
1261
|
+
.mock("POST", "/api/v1/lineage")
|
|
1262
|
+
.match_body(mockito::Matcher::PartialJson(
|
|
1263
|
+
json!({ "eventType": "START" }),
|
|
1264
|
+
))
|
|
1265
|
+
.with_status(200)
|
|
1266
|
+
.expect(1)
|
|
1267
|
+
.create();
|
|
1268
|
+
|
|
1269
|
+
let complete_mock = server
|
|
1270
|
+
.mock("POST", "/api/v1/lineage")
|
|
1271
|
+
.match_body(mockito::Matcher::PartialJson(json!({
|
|
1272
|
+
"eventType": "COMPLETE",
|
|
1273
|
+
"inputs": [{ "namespace": "s3://lakehouse-bronze", "name": "sales/orders" }],
|
|
1274
|
+
"outputs": [{ "namespace": "s3://lakehouse-warehouse", "name": "silver/orders" }]
|
|
1275
|
+
})))
|
|
1276
|
+
.with_status(200)
|
|
1277
|
+
.expect(1)
|
|
1278
|
+
.create();
|
|
1279
|
+
|
|
1280
|
+
// path = raw config paths (default mode); uri = resolved cloud identities.
|
|
1281
|
+
let entity = json!({
|
|
1282
|
+
"name": "orders",
|
|
1283
|
+
"source": {
|
|
1284
|
+
"format": "csv",
|
|
1285
|
+
"storage": "bronze",
|
|
1286
|
+
"uri": "s3://lakehouse-bronze/sales/orders",
|
|
1287
|
+
"path": "sales/orders"
|
|
1288
|
+
},
|
|
1289
|
+
"sinks": {
|
|
1290
|
+
"accepted": {
|
|
1291
|
+
"format": "parquet",
|
|
1292
|
+
"storage": "warehouse",
|
|
1293
|
+
"uri": "s3://lakehouse-warehouse/silver/orders",
|
|
1294
|
+
"path": "silver/orders"
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
"schema": { "columns": [], "primary_key": [], "unique_keys": [] }
|
|
1298
|
+
});
|
|
1299
|
+
let manifest = replay_manifest(&server.url(), json!([entity]));
|
|
1300
|
+
|
|
1301
|
+
let obs = floe_core::lineage::build_observer_from_manifest_json(&manifest, "")
|
|
1302
|
+
.expect("observer builds from manifest")
|
|
1303
|
+
.expect("manifest has a lineage block => Some observer");
|
|
1304
|
+
|
|
1305
|
+
obs.on_event(entity_started("orders"));
|
|
1306
|
+
obs.on_event(entity_finished_event("orders", "success"));
|
|
1307
|
+
|
|
1308
|
+
start_mock.assert();
|
|
1309
|
+
complete_mock.assert();
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// Firing events for only the selected entity (as `--entities orders` does)
|
|
1313
|
+
// still carries that entity's dataset context, because the observer holds all
|
|
1314
|
+
// manifest entities keyed by name.
|
|
1315
|
+
#[test]
|
|
1316
|
+
fn manifest_replay_entity_selection_keeps_dataset_context() {
|
|
1317
|
+
let mut server = mockito::Server::new();
|
|
1318
|
+
|
|
1319
|
+
let start_mock = server
|
|
1320
|
+
.mock("POST", "/api/v1/lineage")
|
|
1321
|
+
.match_body(mockito::Matcher::PartialJson(
|
|
1322
|
+
json!({ "eventType": "START" }),
|
|
1323
|
+
))
|
|
1324
|
+
.with_status(200)
|
|
1325
|
+
.expect(1)
|
|
1326
|
+
.create();
|
|
1327
|
+
|
|
1328
|
+
let complete_mock = server
|
|
1329
|
+
.mock("POST", "/api/v1/lineage")
|
|
1330
|
+
.match_body(mockito::Matcher::PartialJson(json!({
|
|
1331
|
+
"eventType": "COMPLETE",
|
|
1332
|
+
"inputs": [{ "namespace": "s3://lakehouse-bronze", "name": "sales/orders" }],
|
|
1333
|
+
"outputs": [{ "namespace": "s3://lakehouse-warehouse", "name": "silver/orders" }]
|
|
1334
|
+
})))
|
|
1335
|
+
.with_status(200)
|
|
1336
|
+
.expect(1)
|
|
1337
|
+
.create();
|
|
1338
|
+
|
|
1339
|
+
let manifest = replay_manifest(
|
|
1340
|
+
&server.url(),
|
|
1341
|
+
json!([
|
|
1342
|
+
replay_entity(
|
|
1343
|
+
"orders",
|
|
1344
|
+
"s3://lakehouse-bronze/sales/orders",
|
|
1345
|
+
"s3://lakehouse-warehouse/silver/orders",
|
|
1346
|
+
),
|
|
1347
|
+
replay_entity(
|
|
1348
|
+
"customers",
|
|
1349
|
+
"s3://lakehouse-bronze/sales/customers",
|
|
1350
|
+
"s3://lakehouse-warehouse/silver/customers",
|
|
1351
|
+
)
|
|
1352
|
+
]),
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
let obs = floe_core::lineage::build_observer_from_manifest_json(&manifest, "")
|
|
1356
|
+
.expect("observer builds from manifest")
|
|
1357
|
+
.expect("manifest has a lineage block => Some observer");
|
|
1358
|
+
|
|
1359
|
+
// Only "orders" runs; "customers" is not selected.
|
|
1360
|
+
obs.on_event(entity_started("orders"));
|
|
1361
|
+
obs.on_event(entity_finished_event("orders", "success"));
|
|
1362
|
+
|
|
1363
|
+
start_mock.assert();
|
|
1364
|
+
complete_mock.assert();
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// A manifest without a lineage block yields no observer (lineage disabled),
|
|
1368
|
+
// rather than erroring.
|
|
1369
|
+
#[test]
|
|
1370
|
+
fn manifest_replay_without_lineage_block_yields_no_observer() {
|
|
1371
|
+
let manifest = json!({
|
|
1372
|
+
"spec_version": "0.3",
|
|
1373
|
+
"report_base_uri": "file:///tmp",
|
|
1374
|
+
"entities": []
|
|
1375
|
+
})
|
|
1376
|
+
.to_string();
|
|
1377
|
+
|
|
1378
|
+
let obs = floe_core::lineage::build_observer_from_manifest_json(&manifest, "")
|
|
1379
|
+
.expect("no error when lineage block is absent");
|
|
1380
|
+
|
|
1381
|
+
assert!(
|
|
1382
|
+
obs.is_none(),
|
|
1383
|
+
"manifest without lineage block => no observer"
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "floe-python"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.11"
|
|
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.11", 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.11"
|
|
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.11"]
|
|
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
|
|
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
|