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.
Files changed (394) hide show
  1. classifyre_cli-0.7.2/.turbo/turbo-build.log +3 -0
  2. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/PKG-INFO +1 -1
  3. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/package.json +1 -1
  4. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/pyproject.toml +1 -1
  5. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/graph/edges.py +1 -0
  6. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/main.py +8 -0
  7. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/rest.py +16 -0
  8. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/base.py +115 -3
  9. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/custom/source.py +1 -1
  10. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/databricks/source.py +185 -21
  11. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/hive/source.py +4 -1
  12. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mongodb/source.py +1 -0
  13. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mysql/source.py +1 -1
  14. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/neo4j/source.py +3 -0
  15. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/tabular_base.py +33 -0
  16. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/sql_lineage.py +11 -0
  17. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_automatic_new_first.py +1 -1
  18. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_databricks_source.py +234 -0
  19. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_elasticsearch_source.py +1 -1
  20. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_hive_source.py +23 -0
  21. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_meilisearch_source.py +1 -1
  22. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_mysql_source.py +104 -0
  23. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_neo4j_source.py +26 -0
  24. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_opensearch_source.py +1 -1
  25. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sampling_automatic.py +84 -1
  26. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sql_lineage.py +42 -1
  27. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_tabular_automatic_sampling.py +52 -5
  28. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/uv.lock +277 -277
  29. classifyre_cli-0.7.0/.turbo/turbo-build.log +0 -3
  30. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/.gitignore +0 -0
  31. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/.python-version +0 -0
  32. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/README.md +0 -0
  33. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/main.py +0 -0
  34. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/scripts/generate_models.py +0 -0
  35. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/scripts/generate_runtime_packages.py +0 -0
  36. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/scripts/generate_sdk_completions.py +0 -0
  37. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/__init__.py +0 -0
  38. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/__init__.py +0 -0
  39. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/contract.py +0 -0
  40. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/preview.py +0 -0
  41. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/runner.py +0 -0
  42. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/sdk.py +0 -0
  43. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/augmentation/session.py +0 -0
  44. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/config.py +0 -0
  45. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/__init__.py +0 -0
  46. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/base.py +0 -0
  47. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/broken_links/__init__.py +0 -0
  48. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/broken_links/detector.py +0 -0
  49. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/config.py +0 -0
  50. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/content/__init__.py +0 -0
  51. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/__init__.py +0 -0
  52. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/detector.py +0 -0
  53. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/extractor.py +0 -0
  54. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/__init__.py +0 -0
  55. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_base.py +0 -0
  56. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_custom_detector.py +0 -0
  57. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_decision.py +0 -0
  58. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_factory.py +0 -0
  59. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_gliner2.py +0 -0
  60. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_image_classification.py +0 -0
  61. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_llm.py +0 -0
  62. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_object_detection.py +0 -0
  63. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_provider_call.py +0 -0
  64. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_regex.py +0 -0
  65. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_tag.py +0 -0
  66. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/runners/_text_classification.py +0 -0
  67. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom/trainer.py +0 -0
  68. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/__init__.py +0 -0
  69. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/contract.py +0 -0
  70. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/files.py +0 -0
  71. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/preview.py +0 -0
  72. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/results.py +0 -0
  73. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/runner.py +0 -0
  74. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/sdk.py +0 -0
  75. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/custom_detector/session.py +0 -0
  76. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/dependencies.py +0 -0
  77. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/engine_version.py +0 -0
  78. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/errors.py +0 -0
  79. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/pii/__init__.py +0 -0
  80. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/pii/detector.py +0 -0
  81. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/secrets/__init__.py +0 -0
  82. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/secrets/detector.py +0 -0
  83. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/threat/__init__.py +0 -0
  84. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/threat/code_security_detector.py +0 -0
  85. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/detectors/threat/yara_detector.py +0 -0
  86. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/file_evaluation/__init__.py +0 -0
  87. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/file_evaluation/asset_fixture.py +0 -0
  88. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/file_evaluation/runner.py +0 -0
  89. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/graph/__init__.py +0 -0
  90. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/models/generated_detectors.py +0 -0
  91. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/models/generated_input.py +0 -0
  92. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/models/generated_single_asset_scan_results.py +0 -0
  93. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/__init__.py +0 -0
  94. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/child.py +0 -0
  95. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/cli.py +0 -0
  96. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/cohort.py +0 -0
  97. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/contract.py +0 -0
  98. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/execute.py +0 -0
  99. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/files.py +0 -0
  100. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/groups.py +0 -0
  101. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/outputs.py +0 -0
  102. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/packages.py +0 -0
  103. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/protocol.py +0 -0
  104. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/redact.py +0 -0
  105. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/scaffold.py +0 -0
  106. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/sdk.py +0 -0
  107. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/notebook/serialize.py +0 -0
  108. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/__init__.py +0 -0
  109. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/base.py +0 -0
  110. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/console.py +0 -0
  111. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/factory.py +0 -0
  112. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/outputs/file.py +0 -0
  113. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/__init__.py +0 -0
  114. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/asset_payload_server.py +0 -0
  115. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/content_provider.py +0 -0
  116. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/detector_breaker.py +0 -0
  117. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/detector_pipeline.py +0 -0
  118. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/finding_context.py +0 -0
  119. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/media_artifact.py +0 -0
  120. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/parsed_content_provider.py +0 -0
  121. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/payload_window.py +0 -0
  122. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/scan_cache.py +0 -0
  123. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/text_artifact.py +0 -0
  124. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/pipeline/worker_pool.py +0 -0
  125. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/__init__.py +0 -0
  126. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/asset_metadata.py +0 -0
  127. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/atlassian_common.py +0 -0
  128. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/azure_blob_storage/__init__.py +0 -0
  129. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/azure_blob_storage/source.py +0 -0
  130. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/confluence/__init__.py +0 -0
  131. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/confluence/source.py +0 -0
  132. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/custom/__init__.py +0 -0
  133. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/custom/env.py +0 -0
  134. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/custom/runner.py +0 -0
  135. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/databricks/__init__.py +0 -0
  136. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/delta_lake/__init__.py +0 -0
  137. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/delta_lake/source.py +0 -0
  138. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dependencies.py +0 -0
  139. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dremio/__init__.py +0 -0
  140. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dremio/client.py +0 -0
  141. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dremio/source.py +0 -0
  142. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dropbox/__init__.py +0 -0
  143. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dropbox/auth.py +0 -0
  144. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/dropbox/source.py +0 -0
  145. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/elasticsearch/__init__.py +0 -0
  146. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/elasticsearch/source.py +0 -0
  147. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/email/__init__.py +0 -0
  148. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/email/source.py +0 -0
  149. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/git/__init__.py +0 -0
  150. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/git/source.py +0 -0
  151. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/google_cloud_storage/__init__.py +0 -0
  152. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/google_cloud_storage/source.py +0 -0
  153. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/google_workspace/__init__.py +0 -0
  154. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/google_workspace/source.py +0 -0
  155. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/hive/__init__.py +0 -0
  156. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/hugging_face/__init__.py +0 -0
  157. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/hugging_face/source.py +0 -0
  158. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/iceberg/__init__.py +0 -0
  159. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/iceberg/source.py +0 -0
  160. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/jira/__init__.py +0 -0
  161. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/jira/source.py +0 -0
  162. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/kafka/__init__.py +0 -0
  163. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/kafka/rest.py +0 -0
  164. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/kafka/source.py +0 -0
  165. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/lakehouse_base.py +0 -0
  166. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/local_folder/__init__.py +0 -0
  167. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/local_folder/source.py +0 -0
  168. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/meilisearch/__init__.py +0 -0
  169. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/meilisearch/source.py +0 -0
  170. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/microsoft_365/__init__.py +0 -0
  171. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/microsoft_365/source.py +0 -0
  172. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mongodb/__init__.py +0 -0
  173. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mssql/__init__.py +0 -0
  174. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mssql/source.py +0 -0
  175. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/mysql/__init__.py +0 -0
  176. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/neo4j/__init__.py +0 -0
  177. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/notion/__init__.py +0 -0
  178. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/notion/client.py +0 -0
  179. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/notion/source.py +0 -0
  180. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/object_storage/base.py +0 -0
  181. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/opensearch/__init__.py +0 -0
  182. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/opensearch/source.py +0 -0
  183. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/oracle/__init__.py +0 -0
  184. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/oracle/source.py +0 -0
  185. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/postgresql/__init__.py +0 -0
  186. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/postgresql/source.py +0 -0
  187. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/powerbi/__init__.py +0 -0
  188. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/powerbi/source.py +0 -0
  189. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/recipe_normalizer.py +0 -0
  190. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/reddit/__init__.py +0 -0
  191. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/reddit/source.py +0 -0
  192. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/s3_client.py +0 -0
  193. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/s3_compatible_storage/README.md +0 -0
  194. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/s3_compatible_storage/__init__.py +0 -0
  195. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/s3_compatible_storage/source.py +0 -0
  196. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/sandbox/__init__.py +0 -0
  197. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/sandbox/source.py +0 -0
  198. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/search_engine_base.py +0 -0
  199. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/servicedesk/__init__.py +0 -0
  200. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/servicedesk/source.py +0 -0
  201. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/slack/__init__.py +0 -0
  202. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/slack/source.py +0 -0
  203. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/snowflake/__init__.py +0 -0
  204. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/snowflake/source.py +0 -0
  205. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/sqlite/__init__.py +0 -0
  206. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/sqlite/source.py +0 -0
  207. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/tableau/__init__.py +0 -0
  208. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/tableau/source.py +0 -0
  209. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/tabular_utils.py +0 -0
  210. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/wordpress/__init__.py +0 -0
  211. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/wordpress/source.py +0 -0
  212. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/youtube/__init__.py +0 -0
  213. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/sources/youtube/source.py +0 -0
  214. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/telemetry.py +0 -0
  215. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/__init__.py +0 -0
  216. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/archive_extraction.py +0 -0
  217. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/content_extraction.py +0 -0
  218. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/dependency_groups.py +0 -0
  219. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/embedded_files.py +0 -0
  220. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/external_video.py +0 -0
  221. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/file_metadata.py +0 -0
  222. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/file_parser.py +0 -0
  223. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/file_to_images.py +0 -0
  224. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/hashing.py +0 -0
  225. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/legacy_office.py +0 -0
  226. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/payload.py +0 -0
  227. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/range_reader.py +0 -0
  228. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/redaction.py +0 -0
  229. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/resources.py +0 -0
  230. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/sampling_cursor.py +0 -0
  231. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/sampling_frontier.py +0 -0
  232. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/source_files.py +0 -0
  233. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/torch_runtime.py +0 -0
  234. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/transcription.py +0 -0
  235. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/urn.py +0 -0
  236. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/uv_sync.py +0 -0
  237. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/validation.py +0 -0
  238. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/src/utils/video_processing.py +0 -0
  239. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/__init__.py +0 -0
  240. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/_lakehouse_fakes.py +0 -0
  241. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/conftest.py +0 -0
  242. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/__init__.py +0 -0
  243. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  244. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/conftest.py +0 -0
  245. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/content/__init__.py +0 -0
  246. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/__init__.py +0 -0
  247. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/conftest.py +0 -0
  248. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_decision_runner.py +0 -0
  249. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_gliner25_runner.py +0 -0
  250. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  251. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  252. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_llm_runner.py +0 -0
  253. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  254. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_regex_runner.py +0 -0
  255. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_runner_hardening.py +0 -0
  256. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/custom/test_transformer_runners.py +0 -0
  257. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/__init__.py +0 -0
  258. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/conftest.py +0 -0
  259. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/sample_invoice.pdf +0 -0
  260. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_chunking.py +0 -0
  261. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_detector.py +0 -0
  262. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  263. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  264. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/pii/test_pii_severity.py +0 -0
  265. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/secrets/__init__.py +0 -0
  266. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  267. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  268. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_base_detector.py +0 -0
  269. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  270. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_custom_detector_session.py +0 -0
  271. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  272. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_detector_pipeline_types.py +0 -0
  273. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_detector_schema_examples.py +0 -0
  274. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_detector_types.py +0 -0
  275. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_phase2_detectors.py +0 -0
  276. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_registry.py +0 -0
  277. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/test_tag_runner.py +0 -0
  278. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/threat/__init__.py +0 -0
  279. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/threat/test_code_security_detector.py +0 -0
  280. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/detectors/threat/test_yara_detector.py +0 -0
  281. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  282. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/integration/test_wordpress_links_assets.py +0 -0
  283. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/pipeline/test_detector_breaker.py +0 -0
  284. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/pipeline/test_detector_outcomes.py +0 -0
  285. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/pipeline/test_detector_pipeline.py +0 -0
  286. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/pipeline/test_worker_pool.py +0 -0
  287. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_arrow_ipc_payloads.py +0 -0
  288. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_assets_metadata_catalog.py +0 -0
  289. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_contract.py +0 -0
  290. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_edges.py +0 -0
  291. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_patch.py +0 -0
  292. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_scan_cache.py +0 -0
  293. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_sdk.py +0 -0
  294. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_session.py +0 -0
  295. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_augmentation_tags_end_to_end.py +0 -0
  296. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_azure_blob_storage_source.py +0 -0
  297. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_base_source_attachment.py +0 -0
  298. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_base_source_sampling.py +0 -0
  299. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_byte_valued_fields.py +0 -0
  300. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_config.py +0 -0
  301. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_confluence_source.py +0 -0
  302. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_detector_end_to_end.py +0 -0
  303. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_detector_evaluation.py +0 -0
  304. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_detector_notebook.py +0 -0
  305. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_detector_templates.py +0 -0
  306. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_extractor.py +0 -0
  307. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_source.py +0 -0
  308. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_source_relationships.py +0 -0
  309. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_custom_source_tags_end_to_end.py +0 -0
  310. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_delta_lake_source.py +0 -0
  311. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_dependency_groups.py +0 -0
  312. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_detector_scope.py +0 -0
  313. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_dremio_source.py +0 -0
  314. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_dropbox_auth_command.py +0 -0
  315. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_dropbox_source.py +0 -0
  316. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_email_source.py +0 -0
  317. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_file_evaluation_runner.py +0 -0
  318. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_finding_context.py +0 -0
  319. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_git_source.py +0 -0
  320. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_google_cloud_storage_source.py +0 -0
  321. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_google_workspace_source.py +0 -0
  322. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_graph_edges.py +0 -0
  323. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_hashing.py +0 -0
  324. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_hugging_face_source.py +0 -0
  325. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_iceberg_source.py +0 -0
  326. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_jira_source.py +0 -0
  327. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_kafka_source.py +0 -0
  328. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_lineage_loss_reporting.py +0 -0
  329. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_local_folder_source.py +0 -0
  330. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_main.py +0 -0
  331. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_media_embedding.py +0 -0
  332. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_microsoft_365_source.py +0 -0
  333. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_mongodb_source.py +0 -0
  334. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_mssql_source.py +0 -0
  335. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_asset_entity.py +0 -0
  336. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_asset_extract.py +0 -0
  337. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_asset_tags.py +0 -0
  338. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_cohort.py +0 -0
  339. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_execute.py +0 -0
  340. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_files.py +0 -0
  341. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_groups.py +0 -0
  342. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_log_levels.py +0 -0
  343. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_packages.py +0 -0
  344. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_query_assets.py +0 -0
  345. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_sdk_exports.py +0 -0
  346. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_serialize.py +0 -0
  347. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notebook_templates.py +0 -0
  348. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_notion_source.py +0 -0
  349. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_object_storage_download_efficiency.py +0 -0
  350. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_oracle_source.py +0 -0
  351. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_outputs.py +0 -0
  352. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_payload_window.py +0 -0
  353. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_payload_window_parquet.py +0 -0
  354. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_postgresql_source.py +0 -0
  355. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_powerbi_source.py +0 -0
  356. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_range_reading.py +0 -0
  357. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_recipe_normalizer.py +0 -0
  358. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_reddit_source.py +0 -0
  359. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_rest_backpressure.py +0 -0
  360. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_runtime_packages.py +0 -0
  361. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_s3_compatible_storage_source.py +0 -0
  362. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sampling_cursor.py +0 -0
  363. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sampling_frontier.py +0 -0
  364. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sandbox_source.py +0 -0
  365. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_scan_cache.py +0 -0
  366. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sdk_completions.py +0 -0
  367. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_secret_redaction.py +0 -0
  368. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_semantic_declarations.py +0 -0
  369. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_servicedesk_source.py +0 -0
  370. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_slack_source.py +0 -0
  371. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_snowflake_source.py +0 -0
  372. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_source_dependency_groups.py +0 -0
  373. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_source_strategy_conformance.py +0 -0
  374. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_sqlite_source.py +0 -0
  375. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_tableau_source.py +0 -0
  376. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_tabular_lineage.py +0 -0
  377. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_tabular_utils.py +0 -0
  378. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_torch_runtime.py +0 -0
  379. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_urn.py +0 -0
  380. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_uv_sync.py +0 -0
  381. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_view_dependencies_are_not_foreign_keys.py +0 -0
  382. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_wordpress_source.py +0 -0
  383. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_youtube_source.py +0 -0
  384. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/test_youtube_source_integration.py +0 -0
  385. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_archive_extraction.py +0 -0
  386. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_content_extraction.py +0 -0
  387. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_embedded_files.py +0 -0
  388. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_file_metadata.py +0 -0
  389. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_file_parser.py +0 -0
  390. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_file_to_images.py +0 -0
  391. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_legacy_office.py +0 -0
  392. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_payload.py +0 -0
  393. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_transcription.py +0 -0
  394. {classifyre_cli-0.7.0 → classifyre_cli-0.7.2}/tests/utils/test_video_processing.py +0 -0
@@ -0,0 +1,3 @@
1
+ $ uv sync
2
+ Resolved 325 packages in 204ms
3
+ Checked 52 packages in 4ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: classifyre-cli
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Classifyre CLI — scan and classify unstructured data sources
5
5
  License: MIT
6
6
  Keywords: data,ingestion,metadata,pii,secrets,unstructured
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classifyre/cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "build": "uv sync",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "classifyre-cli"
3
- version = "0.7.0"
3
+ version = "0.7.2"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  # Upper bound is load-bearing: onnxruntime publishes no cp315 wheels (notably
@@ -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(self, key: str, *, prev_offset: int, fetched: int) -> None:
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
- next_offset = 0 if fetched < size else prev_offset + fetched
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
- self._record_cursor_key(key, 0 if next_offset >= len(items) else next_offset)
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 _available_columns(self, table_ref: TableRef) -> list[str]:
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: list[str] = []
443
+ columns: dict[str, str] = {}
425
444
  for row in cursor.fetchall():
426
- candidate: Any | None = None
427
- try:
428
- candidate = row[0] # type: ignore[index]
429
- except Exception:
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
- upstream_ref = (
692
- Ref.asset(upstream_hash) if upstream_hash else self._urn_ref_for_key(upstream)
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(table_ref, upstream_refs, table_hash_by_key)
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 self._iter_notebooks():
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, dialect="hive", default_database=view.database
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
  ]