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