classifyre-cli 0.7.0__tar.gz → 0.7.2__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.
- classifyre_cli-0.7.2/.turbo/turbo-build.log +3 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/PKG-INFO +1 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/package.json +1 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/pyproject.toml +1 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/graph/edges.py +1 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/main.py +8 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/rest.py +16 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/base.py +115 -3
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/custom/source.py +1 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/databricks/source.py +185 -21
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/hive/source.py +4 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mongodb/source.py +1 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mysql/source.py +1 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/neo4j/source.py +3 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/tabular_base.py +33 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/sql_lineage.py +11 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_automatic_new_first.py +1 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_databricks_source.py +234 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_elasticsearch_source.py +1 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_hive_source.py +23 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_meilisearch_source.py +1 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_mysql_source.py +104 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_neo4j_source.py +26 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_opensearch_source.py +1 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sampling_automatic.py +84 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sql_lineage.py +42 -1
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_tabular_automatic_sampling.py +52 -5
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/uv.lock +277 -277
- classifyre_cli-0.7.0/.turbo/turbo-build.log +0 -3
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/.gitignore +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/.python-version +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/README.md +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/main.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/scripts/generate_runtime_packages.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/scripts/generate_sdk_completions.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/contract.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/preview.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/sdk.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/session.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/config.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/base.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/config.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_custom_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_decision.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_gliner2.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_provider_call.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_tag.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_text_classification.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/contract.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/files.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/preview.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/results.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/sdk.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/session.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/engine_version.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/errors.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/file_evaluation/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/file_evaluation/asset_fixture.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/file_evaluation/runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/graph/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/models/generated_detectors.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/models/generated_input.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/child.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/cli.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/cohort.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/contract.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/execute.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/files.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/groups.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/outputs.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/packages.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/protocol.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/redact.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/scaffold.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/sdk.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/serialize.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/base.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/console.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/file.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/asset_payload_server.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/detector_breaker.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/detector_pipeline.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/finding_context.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/media_artifact.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/payload_window.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/scan_cache.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/text_artifact.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/custom/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/custom/env.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/custom/runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dremio/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dremio/client.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dremio/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dropbox/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dropbox/auth.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dropbox/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/git/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/git/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/hugging_face/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/hugging_face/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/kafka/rest.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/reddit/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/reddit/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/sandbox/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/sandbox/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/telemetry.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/archive_extraction.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/embedded_files.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/external_video.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/file_parser.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/legacy_office.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/payload.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/range_reader.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/redaction.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/resources.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/sampling_cursor.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/sampling_frontier.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/source_files.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/torch_runtime.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/urn.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/validation.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/video_processing.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/conftest.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_decision_runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_gliner25_runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_transformer_runners.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_chunking.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_custom_detector_session.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_tag_runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/pipeline/test_detector_breaker.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/pipeline/test_detector_outcomes.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/pipeline/test_detector_pipeline.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_arrow_ipc_payloads.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_contract.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_edges.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_patch.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_scan_cache.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_sdk.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_session.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_tags_end_to_end.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_byte_valued_fields.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_config.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_detector_end_to_end.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_detector_evaluation.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_detector_notebook.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_detector_templates.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_source_relationships.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_source_tags_end_to_end.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_detector_scope.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_dremio_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_dropbox_auth_command.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_dropbox_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_file_evaluation_runner.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_finding_context.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_git_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_graph_edges.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_hugging_face_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_lineage_loss_reporting.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_main.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_media_embedding.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_asset_entity.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_asset_extract.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_asset_tags.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_cohort.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_execute.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_files.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_groups.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_log_levels.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_packages.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_query_assets.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_sdk_exports.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_serialize.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_templates.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_object_storage_download_efficiency.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_payload_window.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_payload_window_parquet.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_range_reading.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_reddit_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_rest_backpressure.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_runtime_packages.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sampling_cursor.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sampling_frontier.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sandbox_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_scan_cache.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sdk_completions.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_secret_redaction.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_semantic_declarations.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_tabular_lineage.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_torch_runtime.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_urn.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_view_dependencies_are_not_foreign_keys.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_archive_extraction.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_embedded_files.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_file_parser.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_legacy_office.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_payload.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_transcription.py +0 -0
- {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_video_processing.py +0 -0
|
@@ -131,6 +131,7 @@ class FieldTransform(StrEnum):
|
|
|
131
131
|
IDENTITY = "IDENTITY" # copied through unchanged
|
|
132
132
|
TRANSFORMED = "TRANSFORMED" # an expression combined the upstreams
|
|
133
133
|
AGGREGATED = "AGGREGATED" # SUM/COUNT/... over a group
|
|
134
|
+
MAPPED = "MAPPED" # the platform says it feeds this column, not how
|
|
134
135
|
INDIRECT = "INDIRECT" # influenced the rows, not the values
|
|
135
136
|
|
|
136
137
|
|
|
@@ -971,6 +971,14 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
971
971
|
source.partial_coverage_reason or "no reason given",
|
|
972
972
|
)
|
|
973
973
|
|
|
974
|
+
if getattr(source, "listing_complete", False) and hasattr(
|
|
975
|
+
sink, "set_listing_complete"
|
|
976
|
+
):
|
|
977
|
+
sink.set_listing_complete()
|
|
978
|
+
|
|
979
|
+
if hasattr(sink, "set_sweep_complete"):
|
|
980
|
+
sink.set_sweep_complete(getattr(source, "sweep_complete", None))
|
|
981
|
+
|
|
974
982
|
# Relationships go *before* finish(): finish() finalizes the
|
|
975
983
|
# run and marks it COMPLETED, and edges that land after that
|
|
976
984
|
# belong to a run that already claimed to be done.
|
|
@@ -203,6 +203,10 @@ class FinalizeIngestRunRequest(BaseModel):
|
|
|
203
203
|
# "This run covered a slice of the source." Suppresses retirement: absence
|
|
204
204
|
# from a cohort run is not evidence that an asset is gone.
|
|
205
205
|
partial_coverage: bool | None = Field(None, serialization_alias="partialCoverage")
|
|
206
|
+
discovery_complete: bool | None = Field(None, serialization_alias="discoveryComplete")
|
|
207
|
+
# Whether every unit of an AUTOMATIC sweep that wraps has been covered once.
|
|
208
|
+
# Only sent by a run with a wrapping unit; see BaseSource.sweep_complete.
|
|
209
|
+
sweep_complete: bool | None = Field(None, serialization_alias="sweepComplete")
|
|
206
210
|
# Per ctx.cohort(): keys each band visited, and the weights it ran with.
|
|
207
211
|
cohort_stats: dict[str, Any] | None = Field(None, serialization_alias="cohortStats")
|
|
208
212
|
# Detectors the circuit breaker disabled mid-run (scan-cache identity
|
|
@@ -261,6 +265,8 @@ class RestOutputSink:
|
|
|
261
265
|
self._scan_cache_savings: tuple[int, int] | None = None
|
|
262
266
|
self._partial_coverage: bool = False
|
|
263
267
|
self._partial_coverage_reason: str = ""
|
|
268
|
+
self._listing_complete: bool = False
|
|
269
|
+
self._sweep_complete: bool | None = None
|
|
264
270
|
self._detectors_degraded: list[str] = []
|
|
265
271
|
self._cohort_stats: dict[str, Any] | None = None
|
|
266
272
|
self.relationship_report = RelationshipReport()
|
|
@@ -279,6 +285,14 @@ class RestOutputSink:
|
|
|
279
285
|
if reason:
|
|
280
286
|
self._partial_coverage_reason = str(reason)
|
|
281
287
|
|
|
288
|
+
def set_listing_complete(self) -> None:
|
|
289
|
+
"""Mark that discovery named every object of the configured scope."""
|
|
290
|
+
self._listing_complete = True
|
|
291
|
+
|
|
292
|
+
def set_sweep_complete(self, complete: bool | None) -> None:
|
|
293
|
+
"""Record whether the wrapping sweep has covered all its ground once."""
|
|
294
|
+
self._sweep_complete = complete
|
|
295
|
+
|
|
282
296
|
def set_detectors_degraded(self, keys: list[str]) -> None:
|
|
283
297
|
"""Record detectors the breaker disabled mid-run, for the next run's sweep."""
|
|
284
298
|
seen: set[str] = set(self._detectors_degraded)
|
|
@@ -415,6 +429,8 @@ class RestOutputSink:
|
|
|
415
429
|
relationships_dropped=lineage.dropped,
|
|
416
430
|
relationship_errors=lineage.errors or None,
|
|
417
431
|
partial_coverage=True if self._partial_coverage else None,
|
|
432
|
+
discovery_complete=True if self._listing_complete else None,
|
|
433
|
+
sweep_complete=self._sweep_complete,
|
|
418
434
|
cohort_stats=self._cohort_stats,
|
|
419
435
|
detectors_degraded=self._detectors_degraded or None,
|
|
420
436
|
)
|
|
@@ -89,6 +89,12 @@ class BaseSource(ABC):
|
|
|
89
89
|
self._next_sampling_cursor: dict[str, Any] | None = None
|
|
90
90
|
self._partial_coverage: bool = False
|
|
91
91
|
self._partial_coverage_reason: str = ""
|
|
92
|
+
self._listing_complete: bool = False
|
|
93
|
+
self._listing_errors: int = 0
|
|
94
|
+
# Ground an AUTOMATIC sweep has to cover, by cursor key, and whether
|
|
95
|
+
# each unit has been covered once (see ``sweep_complete``).
|
|
96
|
+
self._sweep_units: dict[str, bool] = {}
|
|
97
|
+
self._sweep_wraps: bool = False
|
|
92
98
|
self._sampling_cursor_lock = threading.Lock()
|
|
93
99
|
self._apply_initial_sampling_override(normalized_recipe)
|
|
94
100
|
recipe.clear()
|
|
@@ -177,6 +183,72 @@ class BaseSource(ABC):
|
|
|
177
183
|
"""
|
|
178
184
|
return self._next_sampling_cursor
|
|
179
185
|
|
|
186
|
+
# ── Has the sweep gone round once? ───────────────────────────────────
|
|
187
|
+
#
|
|
188
|
+
# A cursor that wraps (an offset that returns to 0 at the end, a walk that
|
|
189
|
+
# starts over) never stops moving, by design: walking again is the only way
|
|
190
|
+
# a store that cannot be ordered by age shows its new rows. So "the cursor
|
|
191
|
+
# moved" cannot tell a sweep that is still reaching new ground from one that
|
|
192
|
+
# is going round for the second time, and a scheduler that reads it that way
|
|
193
|
+
# re-runs such a source every minute for good.
|
|
194
|
+
#
|
|
195
|
+
# Each unit of ground therefore records, once, that it has been covered: a
|
|
196
|
+
# frontier says so itself (``done``); a wrapping unit says so by wrapping
|
|
197
|
+
# (kept in the cursor under ``LAPPED_KEY``, which only ever grows). The run
|
|
198
|
+
# reports ``sweep_complete`` when every unit it touched has been covered and
|
|
199
|
+
# at least one of them wraps -- the only case the cursor cannot speak for.
|
|
200
|
+
|
|
201
|
+
#: Cursor key listing the wrapping units that have been covered once.
|
|
202
|
+
LAPPED_KEY = "_lapped"
|
|
203
|
+
|
|
204
|
+
def _lapped_keys(self) -> set[str]:
|
|
205
|
+
"""Wrapping units known to have been covered once, as of this run.
|
|
206
|
+
|
|
207
|
+
Callers hold ``_sampling_cursor_lock`` when they mean to write back.
|
|
208
|
+
"""
|
|
209
|
+
keys: set[str] = set()
|
|
210
|
+
for cursor in (self._sampling_cursor, self._next_sampling_cursor):
|
|
211
|
+
raw = cursor.get(self.LAPPED_KEY) if isinstance(cursor, dict) else None
|
|
212
|
+
if isinstance(raw, list):
|
|
213
|
+
keys.update(str(k) for k in raw)
|
|
214
|
+
return keys
|
|
215
|
+
|
|
216
|
+
def note_sweep_frontier(self, key: str, *, done: bool) -> None:
|
|
217
|
+
"""Record whether the frontier under ``key`` has covered its ground."""
|
|
218
|
+
self._sweep_units[key] = bool(done)
|
|
219
|
+
|
|
220
|
+
def note_sweep_wrap(self, key: str, *, wrapped: bool) -> None:
|
|
221
|
+
"""Record that the wrapping unit ``key`` was read this run.
|
|
222
|
+
|
|
223
|
+
``wrapped`` says this run reached its end and starts over. That is
|
|
224
|
+
remembered in the cursor from then on, so a later run that is
|
|
225
|
+
mid-walk still counts the unit as covered.
|
|
226
|
+
"""
|
|
227
|
+
with self._sampling_cursor_lock:
|
|
228
|
+
self._sweep_wraps = True
|
|
229
|
+
lapped = self._lapped_keys()
|
|
230
|
+
if wrapped and key not in lapped:
|
|
231
|
+
lapped.add(key)
|
|
232
|
+
nxt = (
|
|
233
|
+
self._next_sampling_cursor
|
|
234
|
+
if isinstance(self._next_sampling_cursor, dict)
|
|
235
|
+
else dict(self._sampling_cursor)
|
|
236
|
+
)
|
|
237
|
+
self._next_sampling_cursor = {**nxt, self.LAPPED_KEY: sorted(lapped)}
|
|
238
|
+
self._sweep_units[key] = key in lapped
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def sweep_complete(self) -> bool | None:
|
|
242
|
+
"""Whether every unit this run read has been covered at least once.
|
|
243
|
+
|
|
244
|
+
None when the run has nothing to say: no unit of it wraps, so the
|
|
245
|
+
cursor itself already tells a finished sweep (it stops moving).
|
|
246
|
+
"""
|
|
247
|
+
units = getattr(self, "_sweep_units", None)
|
|
248
|
+
if not getattr(self, "_sweep_wraps", False) or not units:
|
|
249
|
+
return None
|
|
250
|
+
return all(units.values())
|
|
251
|
+
|
|
180
252
|
def declare_partial_coverage(self, reason: str = "") -> None:
|
|
181
253
|
"""Say that this run looked at only part of the source.
|
|
182
254
|
|
|
@@ -191,6 +263,34 @@ class BaseSource(ABC):
|
|
|
191
263
|
if reason and not self._partial_coverage_reason:
|
|
192
264
|
self._partial_coverage_reason = str(reason)
|
|
193
265
|
|
|
266
|
+
def note_listing_error(self) -> None:
|
|
267
|
+
"""Record that part of the catalog could not be listed this run.
|
|
268
|
+
|
|
269
|
+
A listing that skipped a schema, a folder or a label cannot say that
|
|
270
|
+
what it did not list is gone, so it forfeits :meth:`declare_listing_complete`.
|
|
271
|
+
"""
|
|
272
|
+
self._listing_errors = getattr(self, "_listing_errors", 0) + 1
|
|
273
|
+
|
|
274
|
+
def declare_listing_complete(self) -> None:
|
|
275
|
+
"""Say that discovery named every object of the configured scope.
|
|
276
|
+
|
|
277
|
+
Sampling decides how much of each asset is read, not which assets
|
|
278
|
+
exist. A catalog connector (tables, node labels) lists its whole scope
|
|
279
|
+
on every run whatever the strategy, so an asset missing from that list
|
|
280
|
+
is gone and can be retired -- without this, only strategy=ALL ever
|
|
281
|
+
retired anything and every other source kept deleted tables, moved
|
|
282
|
+
databases and renamed labels as "unchanged" forever.
|
|
283
|
+
|
|
284
|
+
Ignored when any part of the listing failed or the run declared partial
|
|
285
|
+
coverage.
|
|
286
|
+
"""
|
|
287
|
+
if getattr(self, "_listing_errors", 0) == 0:
|
|
288
|
+
self._listing_complete = True
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def listing_complete(self) -> bool:
|
|
292
|
+
return bool(getattr(self, "_listing_complete", False)) and not self.partial_coverage
|
|
293
|
+
|
|
194
294
|
@property
|
|
195
295
|
def partial_coverage(self) -> bool:
|
|
196
296
|
return bool(getattr(self, "_partial_coverage", False))
|
|
@@ -231,16 +331,24 @@ class BaseSource(ABC):
|
|
|
231
331
|
saved = self._sampling_cursor.get(key)
|
|
232
332
|
return saved if isinstance(saved, int) and not isinstance(saved, bool) and saved >= 0 else 0
|
|
233
333
|
|
|
234
|
-
def record_automatic_offset(
|
|
334
|
+
def record_automatic_offset(
|
|
335
|
+
self, key: str, *, prev_offset: int, fetched: int, track: bool = True
|
|
336
|
+
) -> None:
|
|
235
337
|
"""Advance a keyed offset cursor; wrap to 0 once a page underfills.
|
|
236
338
|
|
|
237
339
|
For a store that can only be paged by position (``skip``/``OFFSET``)
|
|
238
340
|
and offers nothing to order by, so new rows cannot be told from old
|
|
239
341
|
ones. Wrapping is how such a source ever sees them.
|
|
342
|
+
|
|
343
|
+
``track=False`` keeps the unit out of ``sweep_complete``, for a caller
|
|
344
|
+
whose cursor holds state this offset knows nothing about.
|
|
240
345
|
"""
|
|
241
346
|
size = self.sampling_window_size()
|
|
242
|
-
|
|
347
|
+
wrapped = fetched < size
|
|
348
|
+
next_offset = 0 if wrapped else prev_offset + fetched
|
|
243
349
|
self._record_cursor_key(key, next_offset)
|
|
350
|
+
if track:
|
|
351
|
+
self.note_sweep_wrap(key, wrapped=wrapped)
|
|
244
352
|
|
|
245
353
|
def automatic_window(
|
|
246
354
|
self,
|
|
@@ -314,7 +422,9 @@ class BaseSource(ABC):
|
|
|
314
422
|
)
|
|
315
423
|
window = items[offset : offset + size]
|
|
316
424
|
next_offset = offset + len(window)
|
|
317
|
-
|
|
425
|
+
wrapped = next_offset >= len(items)
|
|
426
|
+
self._record_cursor_key(key, 0 if wrapped else next_offset)
|
|
427
|
+
self.note_sweep_wrap(key, wrapped=wrapped)
|
|
318
428
|
return [item for _, item in window]
|
|
319
429
|
|
|
320
430
|
def automatic_ranged_window(
|
|
@@ -381,6 +491,7 @@ class BaseSource(ABC):
|
|
|
381
491
|
state = frontier.to_state()
|
|
382
492
|
if state is not None:
|
|
383
493
|
self._record_cursor_key(key, state)
|
|
494
|
+
self.note_sweep_frontier(key, done=frontier.done or frontier.fresh)
|
|
384
495
|
return window
|
|
385
496
|
|
|
386
497
|
def automatic_frontier(self, key: str) -> ListFrontier:
|
|
@@ -396,6 +507,7 @@ class BaseSource(ABC):
|
|
|
396
507
|
state = frontier.to_state()
|
|
397
508
|
if state is not None:
|
|
398
509
|
self._record_cursor_key(key, state)
|
|
510
|
+
self.note_sweep_frontier(key, done=frontier.done or frontier.fresh)
|
|
399
511
|
|
|
400
512
|
@staticmethod
|
|
401
513
|
def _read_bool_env(name: str) -> bool | None:
|
|
@@ -622,7 +622,7 @@ class CustomSource(BaseSource):
|
|
|
622
622
|
# P15). The offset is merged in, never substituted.
|
|
623
623
|
self.set_next_sampling_cursor({k: v for k, v in self._sampling_cursor.items() if k != key})
|
|
624
624
|
self.record_automatic_offset(
|
|
625
|
-
key, prev_offset=offset, fetched=int(frame.get("produced") or 0)
|
|
625
|
+
key, prev_offset=offset, fetched=int(frame.get("produced") or 0), track=False
|
|
626
626
|
)
|
|
627
627
|
|
|
628
628
|
def _handled_offset(self, frame: dict[str, Any]) -> bool:
|
|
@@ -7,7 +7,7 @@ from collections.abc import AsyncGenerator, Generator
|
|
|
7
7
|
from contextlib import closing
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from datetime import UTC, datetime, timedelta
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, ClassVar
|
|
11
11
|
from urllib.parse import urlparse
|
|
12
12
|
|
|
13
13
|
import requests
|
|
@@ -19,6 +19,7 @@ from ...graph.edges import (
|
|
|
19
19
|
Method,
|
|
20
20
|
Ref,
|
|
21
21
|
flow,
|
|
22
|
+
same_as,
|
|
22
23
|
)
|
|
23
24
|
from ...models.generated_input import (
|
|
24
25
|
AzureServicePrincipal,
|
|
@@ -68,6 +69,16 @@ class PipelineRef:
|
|
|
68
69
|
state: str | None
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class _ForeignSystem:
|
|
74
|
+
"""The database behind a foreign catalog, as its own connector names it."""
|
|
75
|
+
|
|
76
|
+
platform: str
|
|
77
|
+
authority: str
|
|
78
|
+
database: str | None
|
|
79
|
+
connection: str
|
|
80
|
+
|
|
81
|
+
|
|
71
82
|
def _quote_identifier(identifier: str) -> str:
|
|
72
83
|
return f"`{identifier.replace('`', '``')}`"
|
|
73
84
|
|
|
@@ -109,6 +120,10 @@ class DatabricksSource(BaseTabularSource):
|
|
|
109
120
|
self._validate_auth_configuration()
|
|
110
121
|
|
|
111
122
|
self.session = requests.Session()
|
|
123
|
+
# table_key -> upstream table -> ids of the notebooks Unity Catalog saw
|
|
124
|
+
# move data between the two. Filled by _lineage_refs_for_table.
|
|
125
|
+
self._lineage_notebook_ids: dict[tuple[str, ...], dict[tuple[str, str, str], set[str]]] = {}
|
|
126
|
+
self._foreign_systems: dict[str, _ForeignSystem | None] = {}
|
|
112
127
|
self._access_token: str | None = None
|
|
113
128
|
self._access_token_expiry: datetime | None = None
|
|
114
129
|
|
|
@@ -409,9 +424,13 @@ class DatabricksSource(BaseTabularSource):
|
|
|
409
424
|
without_scheme = raw.split("://", 1)[-1]
|
|
410
425
|
return without_scheme.strip("/").split("/", 1)[0]
|
|
411
426
|
|
|
412
|
-
def
|
|
427
|
+
def _available_column_types(self, table_ref: TableRef) -> dict[str, str]:
|
|
428
|
+
# ``system.information_schema`` sees every catalog from one connection;
|
|
429
|
+
# the default query reads only the connection's current catalog and a
|
|
430
|
+
# parameter style Databricks does not take, so types used to come back
|
|
431
|
+
# empty for every column.
|
|
413
432
|
query = (
|
|
414
|
-
"SELECT column_name "
|
|
433
|
+
"SELECT column_name, COALESCE(full_data_type, data_type) "
|
|
415
434
|
"FROM system.information_schema.columns "
|
|
416
435
|
f"WHERE table_catalog = {_quote_literal(table_ref.database)} "
|
|
417
436
|
f"AND table_schema = {_quote_literal(table_ref.schema or '')} "
|
|
@@ -421,17 +440,17 @@ class DatabricksSource(BaseTabularSource):
|
|
|
421
440
|
conn = self._get_cached_connection(table_ref.database)
|
|
422
441
|
with conn.cursor() as cursor:
|
|
423
442
|
cursor.execute(query)
|
|
424
|
-
columns:
|
|
443
|
+
columns: dict[str, str] = {}
|
|
425
444
|
for row in cursor.fetchall():
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
candidate = None
|
|
431
|
-
if isinstance(candidate, str):
|
|
432
|
-
columns.append(candidate)
|
|
445
|
+
name = row[0] if len(row) > 0 else None
|
|
446
|
+
if isinstance(name, str):
|
|
447
|
+
data_type = row[1] if len(row) > 1 else None
|
|
448
|
+
columns[name] = str(data_type) if data_type is not None else ""
|
|
433
449
|
return columns
|
|
434
450
|
|
|
451
|
+
def _available_columns(self, table_ref: TableRef) -> list[str]:
|
|
452
|
+
return list(self._available_column_types(table_ref))
|
|
453
|
+
|
|
435
454
|
# ── Databricks-specific cell serialization ───────────────────────────
|
|
436
455
|
|
|
437
456
|
def _serialize_cell(self, value: Any) -> str:
|
|
@@ -598,6 +617,7 @@ class DatabricksSource(BaseTabularSource):
|
|
|
598
617
|
try:
|
|
599
618
|
schemas = self._list_schemas_for_catalog(database)
|
|
600
619
|
except Exception as exc:
|
|
620
|
+
self.note_listing_error()
|
|
601
621
|
logger.warning("Skipping catalog %s due to schema listing error: %s", database, exc)
|
|
602
622
|
return tables
|
|
603
623
|
for schema in schemas:
|
|
@@ -606,6 +626,7 @@ class DatabricksSource(BaseTabularSource):
|
|
|
606
626
|
try:
|
|
607
627
|
tables.extend(self._list_tables_for_schema(database, schema))
|
|
608
628
|
except Exception as exc:
|
|
629
|
+
self.note_listing_error()
|
|
609
630
|
logger.warning(
|
|
610
631
|
"Skipping schema %s.%s due to table listing error: %s",
|
|
611
632
|
database,
|
|
@@ -653,14 +674,30 @@ class DatabricksSource(BaseTabularSource):
|
|
|
653
674
|
},
|
|
654
675
|
)
|
|
655
676
|
refs: set[tuple[str, str, str]] = set()
|
|
677
|
+
notebooks: dict[tuple[str, str, str], set[str]] = {}
|
|
656
678
|
for key in ("upstreams", "upstream_tables"):
|
|
657
679
|
entries = response.get(key)
|
|
658
680
|
if isinstance(entries, list):
|
|
659
681
|
for entry in entries:
|
|
660
682
|
if isinstance(entry, dict):
|
|
683
|
+
if (entry.get("tableInfo") or {}).get("has_permission") is False:
|
|
684
|
+
# Unity Catalog knows the upstream and will not name
|
|
685
|
+
# it to this identity, so the table looks sourceless.
|
|
686
|
+
logger.warning(
|
|
687
|
+
"%s has an upstream this identity may not see; "
|
|
688
|
+
"grant it BROWSE on that catalog",
|
|
689
|
+
table_ref.display_name,
|
|
690
|
+
)
|
|
691
|
+
continue
|
|
661
692
|
parsed = self._lineage_table_ref_from_payload(entry)
|
|
662
693
|
if parsed:
|
|
663
694
|
refs.add(parsed)
|
|
695
|
+
for info in entry.get("notebookInfos") or []:
|
|
696
|
+
if isinstance(info, dict) and info.get("notebook_id") is not None:
|
|
697
|
+
notebooks.setdefault(parsed, set()).add(
|
|
698
|
+
str(info["notebook_id"])
|
|
699
|
+
)
|
|
700
|
+
self._lineage_notebook_ids[table_ref.table_key] = notebooks
|
|
664
701
|
return refs
|
|
665
702
|
|
|
666
703
|
def _emit_table_lineage(
|
|
@@ -668,6 +705,7 @@ class DatabricksSource(BaseTabularSource):
|
|
|
668
705
|
table_ref: TableRef,
|
|
669
706
|
upstream_refs: set[tuple[str, str, str]],
|
|
670
707
|
table_hash_by_key: dict[tuple[str, ...], str],
|
|
708
|
+
notebook_hash_by_id: dict[str, str] | None = None,
|
|
671
709
|
) -> None:
|
|
672
710
|
"""Turn Unity Catalog's lineage into typed edges.
|
|
673
711
|
|
|
@@ -675,6 +713,9 @@ class DatabricksSource(BaseTabularSource):
|
|
|
675
713
|
question directly, so nothing here is parsed out of SQL — these edges
|
|
676
714
|
are as trustworthy as lineage gets, and are marked SYSTEM_CATALOG.
|
|
677
715
|
|
|
716
|
+
When Unity Catalog saw a notebook run the statement, the edge carries it
|
|
717
|
+
as ``via`` -- if that notebook is one of this scan's assets.
|
|
718
|
+
|
|
678
719
|
The upstreams that are *not* part of this scan are the important half.
|
|
679
720
|
They used to be dropped on the floor (``if target in table_hash_by_key``),
|
|
680
721
|
which meant a table fed by a catalog outside the configured scope looked
|
|
@@ -686,23 +727,52 @@ class DatabricksSource(BaseTabularSource):
|
|
|
686
727
|
return
|
|
687
728
|
|
|
688
729
|
column_mappings = self._column_mappings_for_table(table_ref)
|
|
730
|
+
notebook_ids = self._lineage_notebook_ids.get(table_ref.table_key, {})
|
|
689
731
|
for upstream in sorted(upstream_refs):
|
|
690
732
|
upstream_hash = table_hash_by_key.get(upstream)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
733
|
+
foreign_urn = self._foreign_urn(upstream)
|
|
734
|
+
upstream_ref: Ref | None
|
|
735
|
+
if foreign_urn:
|
|
736
|
+
upstream_ref = Ref.urn(foreign_urn)
|
|
737
|
+
elif upstream_hash:
|
|
738
|
+
upstream_ref = Ref.asset(upstream_hash)
|
|
739
|
+
else:
|
|
740
|
+
upstream_ref = self._urn_ref_for_key(upstream)
|
|
694
741
|
if upstream_ref is None:
|
|
695
742
|
continue
|
|
743
|
+
via_hash = next(
|
|
744
|
+
(
|
|
745
|
+
notebook_hash_by_id[notebook_id]
|
|
746
|
+
for notebook_id in sorted(notebook_ids.get(upstream, ()))
|
|
747
|
+
if notebook_id in (notebook_hash_by_id or {})
|
|
748
|
+
),
|
|
749
|
+
None,
|
|
750
|
+
)
|
|
696
751
|
self.add_edge(
|
|
697
752
|
flow(
|
|
698
753
|
upstream=upstream_ref,
|
|
699
754
|
downstream=Ref.asset(downstream_hash),
|
|
700
755
|
type=FlowType.TRANSFORM,
|
|
701
756
|
fields=column_mappings.get(upstream, []),
|
|
757
|
+
via=Ref.asset(via_hash) if via_hash else None,
|
|
702
758
|
method=Method.SYSTEM_CATALOG,
|
|
759
|
+
evidence=self._foreign_evidence(upstream) if foreign_urn else None,
|
|
703
760
|
)
|
|
704
761
|
)
|
|
705
762
|
|
|
763
|
+
def _emit_foreign_identity(self, table_ref: TableRef, asset_hash: str) -> None:
|
|
764
|
+
"""A scanned table of a foreign catalog is a mirror of the real table."""
|
|
765
|
+
foreign_urn = self._foreign_urn(table_ref.table_key)
|
|
766
|
+
if not foreign_urn:
|
|
767
|
+
return
|
|
768
|
+
self.add_edge(
|
|
769
|
+
same_as(
|
|
770
|
+
Ref.asset(asset_hash),
|
|
771
|
+
Ref.urn(foreign_urn),
|
|
772
|
+
evidence=self._foreign_evidence(table_ref.table_key),
|
|
773
|
+
)
|
|
774
|
+
)
|
|
775
|
+
|
|
706
776
|
def _column_mappings_for_table(
|
|
707
777
|
self, table_ref: TableRef
|
|
708
778
|
) -> dict[tuple[str, str, str], list[FieldMapping]]:
|
|
@@ -744,14 +814,12 @@ class DatabricksSource(BaseTabularSource):
|
|
|
744
814
|
|
|
745
815
|
return {
|
|
746
816
|
upstream: [
|
|
817
|
+
# Unity Catalog names the columns and nothing between them, so a
|
|
818
|
+
# matching name proves nothing: `lower(email) AS email` matches.
|
|
747
819
|
FieldMapping(
|
|
748
820
|
downstream=downstream,
|
|
749
821
|
upstreams=sorted(sources),
|
|
750
|
-
type=
|
|
751
|
-
FieldTransform.IDENTITY
|
|
752
|
-
if sources == {downstream}
|
|
753
|
-
else FieldTransform.TRANSFORMED
|
|
754
|
-
),
|
|
822
|
+
type=FieldTransform.MAPPED,
|
|
755
823
|
)
|
|
756
824
|
for downstream, sources in sorted(columns.items())
|
|
757
825
|
]
|
|
@@ -767,6 +835,86 @@ class DatabricksSource(BaseTabularSource):
|
|
|
767
835
|
except UrnError:
|
|
768
836
|
return None
|
|
769
837
|
|
|
838
|
+
# ── Foreign catalogs (Lakehouse Federation) ──────────────────────────
|
|
839
|
+
#
|
|
840
|
+
# A foreign catalog is Databricks' name for tables that live in another
|
|
841
|
+
# database. Unity Catalog records lineage through it like any other catalog,
|
|
842
|
+
# so without this the upstream is `databricks://.../app_db/core/customers`,
|
|
843
|
+
# a name no PostgreSQL scan will ever write, and the path stops at the
|
|
844
|
+
# boundary it was most worth crossing.
|
|
845
|
+
|
|
846
|
+
#: UC connection type -> (URN platform, catalog option naming the database).
|
|
847
|
+
#: None where the schema level already is the database, as in MySQL.
|
|
848
|
+
_FOREIGN_PLATFORMS: ClassVar[dict[str, tuple[str, str | None]]] = {
|
|
849
|
+
"POSTGRESQL": ("postgres", "database"),
|
|
850
|
+
"MYSQL": ("mysql", None),
|
|
851
|
+
"SQLSERVER": ("mssql", "database"),
|
|
852
|
+
"ORACLE": ("oracle", "service_name"),
|
|
853
|
+
"SNOWFLAKE": ("snowflake", "database"),
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
def _foreign_system(self, catalog: str) -> _ForeignSystem | None:
|
|
857
|
+
if catalog in self._foreign_systems:
|
|
858
|
+
return self._foreign_systems[catalog]
|
|
859
|
+
system: _ForeignSystem | None = None
|
|
860
|
+
try:
|
|
861
|
+
system = self._read_foreign_system(catalog)
|
|
862
|
+
except Exception as exc:
|
|
863
|
+
# USE CONNECTION is a separate grant. The edge keeps its Databricks
|
|
864
|
+
# name, which is true if not useful, rather than the scan failing.
|
|
865
|
+
logger.warning("Could not read the connection of catalog %s: %s", catalog, exc)
|
|
866
|
+
self._foreign_systems[catalog] = system
|
|
867
|
+
return system
|
|
868
|
+
|
|
869
|
+
def _read_foreign_system(self, catalog: str) -> _ForeignSystem | None:
|
|
870
|
+
info = self._request_json("get", f"/api/2.1/unity-catalog/catalogs/{catalog}")
|
|
871
|
+
connection_name = info.get("connection_name")
|
|
872
|
+
if info.get("catalog_type") != "FOREIGN_CATALOG" or not connection_name:
|
|
873
|
+
return None
|
|
874
|
+
connection = self._request_json(
|
|
875
|
+
"get", f"/api/2.1/unity-catalog/connections/{connection_name}"
|
|
876
|
+
)
|
|
877
|
+
mapping = self._FOREIGN_PLATFORMS.get(str(connection.get("connection_type")))
|
|
878
|
+
if mapping is None:
|
|
879
|
+
return None
|
|
880
|
+
platform, database_option = mapping
|
|
881
|
+
|
|
882
|
+
options = connection.get("options") or {}
|
|
883
|
+
host = str(options.get("host") or "").strip()
|
|
884
|
+
if not host:
|
|
885
|
+
# A URN scoped to the wrong host would stitch two unrelated systems.
|
|
886
|
+
return None
|
|
887
|
+
if platform == "snowflake":
|
|
888
|
+
authority = host.removesuffix(".snowflakecomputing.com")
|
|
889
|
+
else:
|
|
890
|
+
port = options.get("port")
|
|
891
|
+
authority = f"{host}:{port}" if port else host
|
|
892
|
+
|
|
893
|
+
database = None
|
|
894
|
+
if database_option:
|
|
895
|
+
database = str((info.get("options") or {}).get(database_option) or "").strip()
|
|
896
|
+
if not database:
|
|
897
|
+
return None
|
|
898
|
+
return _ForeignSystem(platform, authority, database, str(connection_name))
|
|
899
|
+
|
|
900
|
+
def _foreign_urn(self, table_key: tuple[str, ...]) -> str | None:
|
|
901
|
+
"""The URN the underlying system's own connector writes for this table."""
|
|
902
|
+
system = self._foreign_system(table_key[0])
|
|
903
|
+
if system is None:
|
|
904
|
+
return None
|
|
905
|
+
name = (system.database, *table_key[1:]) if system.database else table_key[1:]
|
|
906
|
+
try:
|
|
907
|
+
return str(Urn.of(system.platform, system.authority, *name))
|
|
908
|
+
except UrnError:
|
|
909
|
+
return None
|
|
910
|
+
|
|
911
|
+
def _foreign_evidence(self, table_key: tuple[str, ...]) -> dict[str, Any]:
|
|
912
|
+
system = self._foreign_system(table_key[0])
|
|
913
|
+
return {
|
|
914
|
+
"foreign_catalog": table_key[0],
|
|
915
|
+
"connection": system.connection if system else None,
|
|
916
|
+
}
|
|
917
|
+
|
|
770
918
|
# ── Notebooks ────────────────────────────────────────────────────────
|
|
771
919
|
|
|
772
920
|
def _iter_notebooks(self) -> Generator[NotebookRef, None, None]:
|
|
@@ -786,6 +934,7 @@ class DatabricksSource(BaseTabularSource):
|
|
|
786
934
|
"get", "/api/2.0/workspace/list", params={"path": path}
|
|
787
935
|
)
|
|
788
936
|
except Exception as exc:
|
|
937
|
+
self.note_listing_error()
|
|
789
938
|
logger.warning("Skipping workspace path %s due to listing error: %s", path, exc)
|
|
790
939
|
continue
|
|
791
940
|
objects = payload.get("objects")
|
|
@@ -865,6 +1014,7 @@ class DatabricksSource(BaseTabularSource):
|
|
|
865
1014
|
try:
|
|
866
1015
|
payload = self._request_json("get", "/api/2.0/pipelines", params=params)
|
|
867
1016
|
except Exception as exc:
|
|
1017
|
+
self.note_listing_error()
|
|
868
1018
|
logger.warning("Could not list Databricks pipelines: %s", exc)
|
|
869
1019
|
break
|
|
870
1020
|
values: list[dict[str, Any]] = []
|
|
@@ -935,6 +1085,15 @@ class DatabricksSource(BaseTabularSource):
|
|
|
935
1085
|
t.table_key: self.generate_hash_id(t.raw_id) for t in tables
|
|
936
1086
|
}
|
|
937
1087
|
|
|
1088
|
+
# Listed before the tables so a lineage edge can name the notebook that
|
|
1089
|
+
# produced it; the notebook assets are still emitted after the tables.
|
|
1090
|
+
notebooks = list(self._iter_notebooks())
|
|
1091
|
+
notebook_hash_by_id = {
|
|
1092
|
+
nb.object_id: self.generate_hash_id(f"notebook_#_{nb.path}")
|
|
1093
|
+
for nb in notebooks
|
|
1094
|
+
if nb.object_id
|
|
1095
|
+
}
|
|
1096
|
+
|
|
938
1097
|
include_lineage = self._extraction_options().include_table_lineage
|
|
939
1098
|
if include_lineage and tables:
|
|
940
1099
|
logger.info("Fetching table lineage for %d table(s)...", len(tables))
|
|
@@ -957,7 +1116,9 @@ class DatabricksSource(BaseTabularSource):
|
|
|
957
1116
|
for target in sorted(upstream_refs)
|
|
958
1117
|
if target in table_hash_by_key
|
|
959
1118
|
]
|
|
960
|
-
self._emit_table_lineage(
|
|
1119
|
+
self._emit_table_lineage(
|
|
1120
|
+
table_ref, upstream_refs, table_hash_by_key, notebook_hash_by_id
|
|
1121
|
+
)
|
|
961
1122
|
except Exception as exc:
|
|
962
1123
|
logger.warning(
|
|
963
1124
|
"Could not resolve Databricks lineage for %s: %s", table_label, exc
|
|
@@ -967,6 +1128,8 @@ class DatabricksSource(BaseTabularSource):
|
|
|
967
1128
|
self._table_lookup[asset.hash] = table_ref
|
|
968
1129
|
batch.append(asset)
|
|
969
1130
|
emitted_tables += 1
|
|
1131
|
+
if include_lineage:
|
|
1132
|
+
self._emit_foreign_identity(table_ref, asset.hash)
|
|
970
1133
|
|
|
971
1134
|
if len(batch) >= self.BATCH_SIZE:
|
|
972
1135
|
yield batch
|
|
@@ -974,7 +1137,7 @@ class DatabricksSource(BaseTabularSource):
|
|
|
974
1137
|
|
|
975
1138
|
# Notebooks
|
|
976
1139
|
notebook_count = 0
|
|
977
|
-
for notebook in
|
|
1140
|
+
for notebook in notebooks:
|
|
978
1141
|
if self._aborted:
|
|
979
1142
|
break
|
|
980
1143
|
batch.append(self._notebook_to_asset(notebook))
|
|
@@ -997,6 +1160,7 @@ class DatabricksSource(BaseTabularSource):
|
|
|
997
1160
|
batch = []
|
|
998
1161
|
if pipeline_count:
|
|
999
1162
|
logger.info("Discovered %d pipeline(s)", pipeline_count)
|
|
1163
|
+
self._declare_listing_if_whole()
|
|
1000
1164
|
|
|
1001
1165
|
if batch:
|
|
1002
1166
|
yield batch
|
|
@@ -293,7 +293,10 @@ class HiveSource(BaseTabularSource):
|
|
|
293
293
|
upstreams = [
|
|
294
294
|
key
|
|
295
295
|
for key in upstream_tables_from_sql(
|
|
296
|
-
definition,
|
|
296
|
+
definition,
|
|
297
|
+
dialect="hive",
|
|
298
|
+
default_database=view.database,
|
|
299
|
+
two_level=True,
|
|
297
300
|
)
|
|
298
301
|
if key != view.table_key and key in scoped
|
|
299
302
|
]
|