classifyre-cli 0.7.1__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.1 → classifyre_cli-0.7.2}/.turbo/turbo-build.log +1 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/PKG-INFO +1 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/package.json +1 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/pyproject.toml +1 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/graph/edges.py +1 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/main.py +3 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/outputs/rest.py +9 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/base.py +85 -3
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/custom/source.py +1 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/databricks/source.py +129 -9
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/mongodb/source.py +1 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/tabular_base.py +18 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_automatic_new_first.py +1 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_databricks_source.py +144 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_elasticsearch_source.py +1 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_meilisearch_source.py +1 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_opensearch_source.py +1 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_sampling_automatic.py +84 -1
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_tabular_automatic_sampling.py +52 -5
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/uv.lock +227 -227
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/.gitignore +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/.python-version +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/README.md +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/main.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/scripts/generate_runtime_packages.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/scripts/generate_sdk_completions.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/augmentation/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/augmentation/contract.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/augmentation/preview.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/augmentation/runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/augmentation/sdk.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/augmentation/session.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/config.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/base.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/config.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_custom_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_decision.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_gliner2.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_provider_call.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_tag.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_text_classification.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom_detector/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom_detector/contract.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom_detector/files.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom_detector/preview.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom_detector/results.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom_detector/runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom_detector/sdk.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/custom_detector/session.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/engine_version.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/errors.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/file_evaluation/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/file_evaluation/asset_fixture.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/file_evaluation/runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/graph/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/models/generated_detectors.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/models/generated_input.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/child.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/cli.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/cohort.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/contract.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/execute.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/files.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/groups.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/outputs.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/packages.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/protocol.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/redact.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/scaffold.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/sdk.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/notebook/serialize.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/outputs/base.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/outputs/console.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/outputs/file.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/asset_payload_server.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/detector_breaker.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/detector_pipeline.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/finding_context.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/media_artifact.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/payload_window.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/scan_cache.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/text_artifact.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/custom/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/custom/env.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/custom/runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/dremio/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/dremio/client.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/dremio/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/dropbox/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/dropbox/auth.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/dropbox/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/git/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/git/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/hugging_face/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/hugging_face/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/kafka/rest.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/reddit/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/reddit/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/sandbox/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/sandbox/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/telemetry.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/archive_extraction.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/embedded_files.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/external_video.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/file_parser.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/legacy_office.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/payload.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/range_reader.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/redaction.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/resources.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/sampling_cursor.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/sampling_frontier.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/source_files.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/sql_lineage.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/torch_runtime.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/urn.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/validation.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/src/utils/video_processing.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/conftest.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/test_decision_runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/test_gliner25_runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/custom/test_transformer_runners.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_chunking.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_custom_detector_session.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/test_tag_runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/pipeline/test_detector_breaker.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/pipeline/test_detector_outcomes.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/pipeline/test_detector_pipeline.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_arrow_ipc_payloads.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_augmentation_contract.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_augmentation_edges.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_augmentation_patch.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_augmentation_scan_cache.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_augmentation_sdk.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_augmentation_session.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_augmentation_tags_end_to_end.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_byte_valued_fields.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_config.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_custom_detector_end_to_end.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_custom_detector_evaluation.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_custom_detector_notebook.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_custom_detector_templates.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_custom_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_custom_source_relationships.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_custom_source_tags_end_to_end.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_detector_scope.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_dremio_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_dropbox_auth_command.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_dropbox_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_file_evaluation_runner.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_finding_context.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_git_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_graph_edges.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_hugging_face_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_lineage_loss_reporting.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_main.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_media_embedding.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_asset_entity.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_asset_extract.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_asset_tags.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_cohort.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_execute.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_files.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_groups.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_log_levels.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_packages.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_query_assets.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_sdk_exports.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_serialize.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notebook_templates.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_object_storage_download_efficiency.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_payload_window.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_payload_window_parquet.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_range_reading.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_reddit_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_rest_backpressure.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_runtime_packages.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_sampling_cursor.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_sampling_frontier.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_sandbox_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_scan_cache.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_sdk_completions.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_secret_redaction.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_semantic_declarations.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_sql_lineage.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_tabular_lineage.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_torch_runtime.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_urn.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_view_dependencies_are_not_foreign_keys.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/utils/test_archive_extraction.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/utils/test_embedded_files.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/utils/test_file_parser.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/utils/test_legacy_office.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/utils/test_payload.py +0 -0
- {classifyre_cli-0.7.1 → classifyre_cli-0.7.2}/tests/utils/test_transcription.py +0 -0
- {classifyre_cli-0.7.1 → 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
|
|
|
@@ -976,6 +976,9 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
976
976
|
):
|
|
977
977
|
sink.set_listing_complete()
|
|
978
978
|
|
|
979
|
+
if hasattr(sink, "set_sweep_complete"):
|
|
980
|
+
sink.set_sweep_complete(getattr(source, "sweep_complete", None))
|
|
981
|
+
|
|
979
982
|
# Relationships go *before* finish(): finish() finalizes the
|
|
980
983
|
# run and marks it COMPLETED, and edges that land after that
|
|
981
984
|
# belong to a run that already claimed to be done.
|
|
@@ -204,6 +204,9 @@ class FinalizeIngestRunRequest(BaseModel):
|
|
|
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
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")
|
|
207
210
|
# Per ctx.cohort(): keys each band visited, and the weights it ran with.
|
|
208
211
|
cohort_stats: dict[str, Any] | None = Field(None, serialization_alias="cohortStats")
|
|
209
212
|
# Detectors the circuit breaker disabled mid-run (scan-cache identity
|
|
@@ -263,6 +266,7 @@ class RestOutputSink:
|
|
|
263
266
|
self._partial_coverage: bool = False
|
|
264
267
|
self._partial_coverage_reason: str = ""
|
|
265
268
|
self._listing_complete: bool = False
|
|
269
|
+
self._sweep_complete: bool | None = None
|
|
266
270
|
self._detectors_degraded: list[str] = []
|
|
267
271
|
self._cohort_stats: dict[str, Any] | None = None
|
|
268
272
|
self.relationship_report = RelationshipReport()
|
|
@@ -285,6 +289,10 @@ class RestOutputSink:
|
|
|
285
289
|
"""Mark that discovery named every object of the configured scope."""
|
|
286
290
|
self._listing_complete = True
|
|
287
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
|
+
|
|
288
296
|
def set_detectors_degraded(self, keys: list[str]) -> None:
|
|
289
297
|
"""Record detectors the breaker disabled mid-run, for the next run's sweep."""
|
|
290
298
|
seen: set[str] = set(self._detectors_degraded)
|
|
@@ -422,6 +430,7 @@ class RestOutputSink:
|
|
|
422
430
|
relationship_errors=lineage.errors or None,
|
|
423
431
|
partial_coverage=True if self._partial_coverage else None,
|
|
424
432
|
discovery_complete=True if self._listing_complete else None,
|
|
433
|
+
sweep_complete=self._sweep_complete,
|
|
425
434
|
cohort_stats=self._cohort_stats,
|
|
426
435
|
detectors_degraded=self._detectors_degraded or None,
|
|
427
436
|
)
|
|
@@ -91,6 +91,10 @@ class BaseSource(ABC):
|
|
|
91
91
|
self._partial_coverage_reason: str = ""
|
|
92
92
|
self._listing_complete: bool = False
|
|
93
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
|
|
94
98
|
self._sampling_cursor_lock = threading.Lock()
|
|
95
99
|
self._apply_initial_sampling_override(normalized_recipe)
|
|
96
100
|
recipe.clear()
|
|
@@ -179,6 +183,72 @@ class BaseSource(ABC):
|
|
|
179
183
|
"""
|
|
180
184
|
return self._next_sampling_cursor
|
|
181
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
|
+
|
|
182
252
|
def declare_partial_coverage(self, reason: str = "") -> None:
|
|
183
253
|
"""Say that this run looked at only part of the source.
|
|
184
254
|
|
|
@@ -261,16 +331,24 @@ class BaseSource(ABC):
|
|
|
261
331
|
saved = self._sampling_cursor.get(key)
|
|
262
332
|
return saved if isinstance(saved, int) and not isinstance(saved, bool) and saved >= 0 else 0
|
|
263
333
|
|
|
264
|
-
def record_automatic_offset(
|
|
334
|
+
def record_automatic_offset(
|
|
335
|
+
self, key: str, *, prev_offset: int, fetched: int, track: bool = True
|
|
336
|
+
) -> None:
|
|
265
337
|
"""Advance a keyed offset cursor; wrap to 0 once a page underfills.
|
|
266
338
|
|
|
267
339
|
For a store that can only be paged by position (``skip``/``OFFSET``)
|
|
268
340
|
and offers nothing to order by, so new rows cannot be told from old
|
|
269
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.
|
|
270
345
|
"""
|
|
271
346
|
size = self.sampling_window_size()
|
|
272
|
-
|
|
347
|
+
wrapped = fetched < size
|
|
348
|
+
next_offset = 0 if wrapped else prev_offset + fetched
|
|
273
349
|
self._record_cursor_key(key, next_offset)
|
|
350
|
+
if track:
|
|
351
|
+
self.note_sweep_wrap(key, wrapped=wrapped)
|
|
274
352
|
|
|
275
353
|
def automatic_window(
|
|
276
354
|
self,
|
|
@@ -344,7 +422,9 @@ class BaseSource(ABC):
|
|
|
344
422
|
)
|
|
345
423
|
window = items[offset : offset + size]
|
|
346
424
|
next_offset = offset + len(window)
|
|
347
|
-
|
|
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)
|
|
348
428
|
return [item for _, item in window]
|
|
349
429
|
|
|
350
430
|
def automatic_ranged_window(
|
|
@@ -411,6 +491,7 @@ class BaseSource(ABC):
|
|
|
411
491
|
state = frontier.to_state()
|
|
412
492
|
if state is not None:
|
|
413
493
|
self._record_cursor_key(key, state)
|
|
494
|
+
self.note_sweep_frontier(key, done=frontier.done or frontier.fresh)
|
|
414
495
|
return window
|
|
415
496
|
|
|
416
497
|
def automatic_frontier(self, key: str) -> ListFrontier:
|
|
@@ -426,6 +507,7 @@ class BaseSource(ABC):
|
|
|
426
507
|
state = frontier.to_state()
|
|
427
508
|
if state is not None:
|
|
428
509
|
self._record_cursor_key(key, state)
|
|
510
|
+
self.note_sweep_frontier(key, done=frontier.done or frontier.fresh)
|
|
429
511
|
|
|
430
512
|
@staticmethod
|
|
431
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
|
|
|
@@ -112,6 +123,7 @@ class DatabricksSource(BaseTabularSource):
|
|
|
112
123
|
# table_key -> upstream table -> ids of the notebooks Unity Catalog saw
|
|
113
124
|
# move data between the two. Filled by _lineage_refs_for_table.
|
|
114
125
|
self._lineage_notebook_ids: dict[tuple[str, ...], dict[tuple[str, str, str], set[str]]] = {}
|
|
126
|
+
self._foreign_systems: dict[str, _ForeignSystem | None] = {}
|
|
115
127
|
self._access_token: str | None = None
|
|
116
128
|
self._access_token_expiry: datetime | None = None
|
|
117
129
|
|
|
@@ -668,6 +680,15 @@ class DatabricksSource(BaseTabularSource):
|
|
|
668
680
|
if isinstance(entries, list):
|
|
669
681
|
for entry in entries:
|
|
670
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
|
|
671
692
|
parsed = self._lineage_table_ref_from_payload(entry)
|
|
672
693
|
if parsed:
|
|
673
694
|
refs.add(parsed)
|
|
@@ -709,9 +730,14 @@ class DatabricksSource(BaseTabularSource):
|
|
|
709
730
|
notebook_ids = self._lineage_notebook_ids.get(table_ref.table_key, {})
|
|
710
731
|
for upstream in sorted(upstream_refs):
|
|
711
732
|
upstream_hash = table_hash_by_key.get(upstream)
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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)
|
|
715
741
|
if upstream_ref is None:
|
|
716
742
|
continue
|
|
717
743
|
via_hash = next(
|
|
@@ -730,9 +756,23 @@ class DatabricksSource(BaseTabularSource):
|
|
|
730
756
|
fields=column_mappings.get(upstream, []),
|
|
731
757
|
via=Ref.asset(via_hash) if via_hash else None,
|
|
732
758
|
method=Method.SYSTEM_CATALOG,
|
|
759
|
+
evidence=self._foreign_evidence(upstream) if foreign_urn else None,
|
|
733
760
|
)
|
|
734
761
|
)
|
|
735
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
|
+
|
|
736
776
|
def _column_mappings_for_table(
|
|
737
777
|
self, table_ref: TableRef
|
|
738
778
|
) -> dict[tuple[str, str, str], list[FieldMapping]]:
|
|
@@ -774,14 +814,12 @@ class DatabricksSource(BaseTabularSource):
|
|
|
774
814
|
|
|
775
815
|
return {
|
|
776
816
|
upstream: [
|
|
817
|
+
# Unity Catalog names the columns and nothing between them, so a
|
|
818
|
+
# matching name proves nothing: `lower(email) AS email` matches.
|
|
777
819
|
FieldMapping(
|
|
778
820
|
downstream=downstream,
|
|
779
821
|
upstreams=sorted(sources),
|
|
780
|
-
type=
|
|
781
|
-
FieldTransform.IDENTITY
|
|
782
|
-
if sources == {downstream}
|
|
783
|
-
else FieldTransform.TRANSFORMED
|
|
784
|
-
),
|
|
822
|
+
type=FieldTransform.MAPPED,
|
|
785
823
|
)
|
|
786
824
|
for downstream, sources in sorted(columns.items())
|
|
787
825
|
]
|
|
@@ -797,6 +835,86 @@ class DatabricksSource(BaseTabularSource):
|
|
|
797
835
|
except UrnError:
|
|
798
836
|
return None
|
|
799
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
|
+
|
|
800
918
|
# ── Notebooks ────────────────────────────────────────────────────────
|
|
801
919
|
|
|
802
920
|
def _iter_notebooks(self) -> Generator[NotebookRef, None, None]:
|
|
@@ -1010,6 +1128,8 @@ class DatabricksSource(BaseTabularSource):
|
|
|
1010
1128
|
self._table_lookup[asset.hash] = table_ref
|
|
1011
1129
|
batch.append(asset)
|
|
1012
1130
|
emitted_tables += 1
|
|
1131
|
+
if include_lineage:
|
|
1132
|
+
self._emit_foreign_identity(table_ref, asset.hash)
|
|
1013
1133
|
|
|
1014
1134
|
if len(batch) >= self.BATCH_SIZE:
|
|
1015
1135
|
yield batch
|
|
@@ -507,6 +507,7 @@ class MongoDBSource(BaseSource):
|
|
|
507
507
|
if next_state.get(name) is not None:
|
|
508
508
|
next_state[name] = self._encode_id(next_state[name])
|
|
509
509
|
self._record_cursor_key(key, next_state)
|
|
510
|
+
self.note_sweep_frontier(key, done=frontier.done or frontier.fresh)
|
|
510
511
|
return documents
|
|
511
512
|
|
|
512
513
|
def _sample_collection_documents(self, collection_ref: CollectionRef) -> list[dict[str, Any]]:
|
|
@@ -703,6 +703,18 @@ class BaseTabularSource(BaseSource):
|
|
|
703
703
|
nxt["tables"] = tables
|
|
704
704
|
self._next_sampling_cursor = nxt
|
|
705
705
|
self._automatic_advanced.add(key)
|
|
706
|
+
self._note_table_sweep(key, state)
|
|
707
|
+
|
|
708
|
+
def _note_table_sweep(self, key: str, state: dict[str, Any] | None) -> None:
|
|
709
|
+
"""Tell the sweep tracker whether this table has been covered once."""
|
|
710
|
+
if state is None:
|
|
711
|
+
self.note_sweep_frontier(key, done=True)
|
|
712
|
+
elif state.get("mode") == self._WRAP_MODE:
|
|
713
|
+
# Walked and wrapped: covered once the walk has reached the end.
|
|
714
|
+
self._sweep_wraps = True
|
|
715
|
+
self._sweep_units[key] = bool(state.get("lapped"))
|
|
716
|
+
else:
|
|
717
|
+
self.note_sweep_frontier(key, done=bool(state.get("done")) or not state.get("head"))
|
|
706
718
|
|
|
707
719
|
def _fetch_page_key_range(
|
|
708
720
|
self,
|
|
@@ -850,6 +862,7 @@ class BaseTabularSource(BaseSource):
|
|
|
850
862
|
)
|
|
851
863
|
# Exhausted (or a key that cannot be stored, e.g. binary) leaves no
|
|
852
864
|
# position, so the next run starts the walk again.
|
|
865
|
+
exhausted = len(rows) < rows_per_page
|
|
853
866
|
if rows and len(rows) >= rows_per_page:
|
|
854
867
|
last_row = rows[-1]
|
|
855
868
|
position = [
|
|
@@ -860,8 +873,13 @@ class BaseTabularSource(BaseSource):
|
|
|
860
873
|
else:
|
|
861
874
|
offset = int(saved.get("offset") or 0)
|
|
862
875
|
rows, column_names = self._fetch_one_page(table_ref, base_query, rows_per_page, offset)
|
|
876
|
+
exhausted = len(rows) < rows_per_page
|
|
863
877
|
if rows and len(rows) >= rows_per_page:
|
|
864
878
|
next_state["offset"] = offset + len(rows)
|
|
879
|
+
# Sticky: once the walk has reached the end, the table counts as
|
|
880
|
+
# covered however many times it is walked again.
|
|
881
|
+
if exhausted or saved.get("lapped"):
|
|
882
|
+
next_state["lapped"] = True
|
|
865
883
|
|
|
866
884
|
self._record_table_cursor(key, next_state)
|
|
867
885
|
|
|
@@ -378,7 +378,7 @@ def test_mongodb_wraps_when_ids_do_not_order_by_age(monkeypatch: pytest.MonkeyPa
|
|
|
378
378
|
assert cursor == {"collection:app.events": 10}
|
|
379
379
|
second, cursor = _mongo_run(monkeypatch, collection, cursor)
|
|
380
380
|
assert len(first) == 10 and len(second) == 5
|
|
381
|
-
assert cursor == {"collection:app.events": 0}
|
|
381
|
+
assert cursor == {"collection:app.events": 0, "_lapped": ["collection:app.events"]}
|
|
382
382
|
# Round again: with nothing to tell new from old, this is how new ones are seen.
|
|
383
383
|
assert len(_mongo_run(monkeypatch, collection, cursor)[0]) == 10
|
|
384
384
|
|
|
@@ -487,6 +487,150 @@ def test_databricks_lineage_edge_names_the_notebook_that_wrote_it(
|
|
|
487
487
|
assert edges["h-payments"].via_id is None
|
|
488
488
|
|
|
489
489
|
|
|
490
|
+
def _foreign_catalogs(path: str) -> dict[str, Any]:
|
|
491
|
+
return {
|
|
492
|
+
"/api/2.1/unity-catalog/catalogs/app_db": {
|
|
493
|
+
"catalog_type": "FOREIGN_CATALOG",
|
|
494
|
+
"connection_name": "app_conn",
|
|
495
|
+
"options": {"database": "shop"},
|
|
496
|
+
},
|
|
497
|
+
"/api/2.1/unity-catalog/connections/app_conn": {
|
|
498
|
+
"connection_type": "POSTGRESQL",
|
|
499
|
+
"options": {"host": "pg.example.test", "port": "5432"},
|
|
500
|
+
},
|
|
501
|
+
"/api/2.1/unity-catalog/catalogs/hr_db": {
|
|
502
|
+
"catalog_type": "FOREIGN_CATALOG",
|
|
503
|
+
"connection_name": "hr_conn",
|
|
504
|
+
},
|
|
505
|
+
"/api/2.1/unity-catalog/connections/hr_conn": {
|
|
506
|
+
"connection_type": "MYSQL",
|
|
507
|
+
"options": {"host": "my.example.test", "port": "10218"},
|
|
508
|
+
},
|
|
509
|
+
"/api/2.1/unity-catalog/catalogs/main": {"catalog_type": "MANAGED_CATALOG"},
|
|
510
|
+
}[path]
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def test_databricks_foreign_catalog_upstream_is_named_by_its_real_system(
|
|
514
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
515
|
+
) -> None:
|
|
516
|
+
source = DatabricksSource(_pat_recipe())
|
|
517
|
+
monkeypatch.setattr(source, "_request_json", lambda _m, path, **_k: _foreign_catalogs(path))
|
|
518
|
+
downstream = TableRef(database="main", schema="mart", table="daily", object_type="TABLE")
|
|
519
|
+
|
|
520
|
+
source._emit_table_lineage(
|
|
521
|
+
downstream,
|
|
522
|
+
{("app_db", "core", "customers"), ("hr_db", "people", "Staff")},
|
|
523
|
+
{downstream.table_key: "h-daily"},
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
urns = {edge.from_urn: edge for edge in source.drain_edges()}
|
|
527
|
+
# What the PostgreSQL and MySQL connectors write for the same tables.
|
|
528
|
+
assert set(urns) == {
|
|
529
|
+
"postgres://pg.example.test/shop/core/customers",
|
|
530
|
+
"mysql://my.example.test:10218/people/staff",
|
|
531
|
+
}
|
|
532
|
+
evidence = urns["postgres://pg.example.test/shop/core/customers"].evidence
|
|
533
|
+
assert evidence["foreign_catalog"] == "app_db"
|
|
534
|
+
assert evidence["connection"] == "app_conn"
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def test_databricks_scanned_foreign_table_is_tied_to_the_table_it_mirrors(
|
|
538
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
539
|
+
) -> None:
|
|
540
|
+
source = DatabricksSource(_pat_recipe())
|
|
541
|
+
monkeypatch.setattr(source, "_request_json", lambda _m, path, **_k: _foreign_catalogs(path))
|
|
542
|
+
downstream = TableRef(database="main", schema="mart", table="daily", object_type="TABLE")
|
|
543
|
+
foreign = TableRef(database="app_db", schema="core", table="orders", object_type="TABLE")
|
|
544
|
+
|
|
545
|
+
source._emit_table_lineage(
|
|
546
|
+
downstream,
|
|
547
|
+
{foreign.table_key},
|
|
548
|
+
{downstream.table_key: "h-daily", foreign.table_key: "h-foreign"},
|
|
549
|
+
)
|
|
550
|
+
source._emit_foreign_identity(foreign, "h-foreign")
|
|
551
|
+
|
|
552
|
+
edges = source.drain_edges()
|
|
553
|
+
# Lineage goes straight to the real table, and the mirror is the same thing.
|
|
554
|
+
assert [(e.relation_class, e.from_urn) for e in edges if e.relation_class == "FLOW"] == [
|
|
555
|
+
("FLOW", "postgres://pg.example.test/shop/core/orders")
|
|
556
|
+
]
|
|
557
|
+
same = [e for e in edges if e.relation_class == "IDENTITY"]
|
|
558
|
+
assert [(e.from_hash, e.to_urn) for e in same] == [
|
|
559
|
+
("h-foreign", "postgres://pg.example.test/shop/core/orders")
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def test_databricks_foreign_catalog_it_cannot_read_stays_a_databricks_name(
|
|
564
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
565
|
+
) -> None:
|
|
566
|
+
# No USE CONNECTION for the scan principal: the edge must survive unbound.
|
|
567
|
+
source = DatabricksSource(_pat_recipe())
|
|
568
|
+
|
|
569
|
+
def _denied(_method: str, path: str, **_k: Any) -> dict[str, Any]:
|
|
570
|
+
if "/connections/" in path:
|
|
571
|
+
raise RuntimeError("PERMISSION_DENIED")
|
|
572
|
+
return _foreign_catalogs(path)
|
|
573
|
+
|
|
574
|
+
monkeypatch.setattr(source, "_request_json", _denied)
|
|
575
|
+
downstream = TableRef(database="main", schema="mart", table="daily", object_type="TABLE")
|
|
576
|
+
|
|
577
|
+
source._emit_table_lineage(
|
|
578
|
+
downstream, {("app_db", "core", "customers")}, {downstream.table_key: "h-daily"}
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
assert [edge.from_urn for edge in source.drain_edges()] == [
|
|
582
|
+
"databricks://adb-123410678901234106.7.azuredatabricks.net/app_db/core/customers"
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def test_databricks_column_lineage_does_not_claim_a_copy_from_a_matching_name(
|
|
587
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
588
|
+
) -> None:
|
|
589
|
+
source = DatabricksSource(
|
|
590
|
+
_pat_recipe(
|
|
591
|
+
optional={"extraction": {"include_table_lineage": True, "include_column_lineage": True}}
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
downstream = TableRef(database="main", schema="mart", table="daily", object_type="TABLE")
|
|
595
|
+
monkeypatch.setattr(source, "_cached_columns", lambda _t: ["email"])
|
|
596
|
+
monkeypatch.setattr(
|
|
597
|
+
source,
|
|
598
|
+
"_request_json",
|
|
599
|
+
lambda *_a, **_k: {
|
|
600
|
+
"upstream_cols": [
|
|
601
|
+
{
|
|
602
|
+
"catalog_name": "main",
|
|
603
|
+
"schema_name": "raw",
|
|
604
|
+
"table_name": "users",
|
|
605
|
+
"name": "email",
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
},
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
mappings = source._column_mappings_for_table(downstream)
|
|
612
|
+
|
|
613
|
+
assert [m.type.value for m in mappings[("main", "raw", "users")]] == ["MAPPED"]
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def test_databricks_upstream_hidden_by_permissions_is_reported(
|
|
617
|
+
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
618
|
+
) -> None:
|
|
619
|
+
source = DatabricksSource(_pat_recipe())
|
|
620
|
+
downstream = TableRef(database="main", schema="mart", table="daily", object_type="TABLE")
|
|
621
|
+
monkeypatch.setattr(
|
|
622
|
+
source,
|
|
623
|
+
"_request_json",
|
|
624
|
+
lambda *_a, **_k: {"upstreams": [{"tableInfo": {"has_permission": False}}]},
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
with caplog.at_level("WARNING"):
|
|
628
|
+
refs = source._lineage_refs_for_table(downstream)
|
|
629
|
+
|
|
630
|
+
assert refs == set()
|
|
631
|
+
assert "grant it BROWSE" in caplog.text
|
|
632
|
+
|
|
633
|
+
|
|
490
634
|
def test_databricks_unlistable_schema_forfeits_the_complete_declaration(
|
|
491
635
|
monkeypatch: pytest.MonkeyPatch,
|
|
492
636
|
) -> None:
|
|
@@ -245,7 +245,7 @@ def test_elasticsearch_automatic_strategy_resumes_and_wraps_on_underfill(
|
|
|
245
245
|
assert body["from"] == 10
|
|
246
246
|
# Only 2 of 12 docs remain from offset 10 → underfilled page → wraps to 0.
|
|
247
247
|
assert len(docs) == 2
|
|
248
|
-
assert src.current_sampling_cursor() == {"index:orders": 0}
|
|
248
|
+
assert src.current_sampling_cursor() == {"index:orders": 0, "_lapped": ["index:orders"]}
|
|
249
249
|
|
|
250
250
|
|
|
251
251
|
async def test_elasticsearch_all_strategy_scrolls_the_whole_index(
|