classifyre-cli 0.6.11__tar.gz → 0.6.12__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 (391) hide show
  1. classifyre_cli-0.6.12/.turbo/turbo-build.log +3 -0
  2. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/PKG-INFO +1 -1
  3. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/package.json +1 -1
  4. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/pyproject.toml +1 -1
  5. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/main.py +33 -1
  6. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/rest.py +22 -6
  7. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/detector_pipeline.py +55 -0
  8. classifyre_cli-0.6.12/src/pipeline/finding_context.py +99 -0
  9. classifyre_cli-0.6.12/src/pipeline/media_artifact.py +262 -0
  10. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/scan_cache.py +58 -2
  11. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/text_artifact.py +20 -1
  12. classifyre_cli-0.6.12/tests/test_finding_context.py +79 -0
  13. classifyre_cli-0.6.12/tests/test_media_embedding.py +437 -0
  14. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/uv.lock +116 -116
  15. classifyre_cli-0.6.11/.turbo/turbo-build.log +0 -3
  16. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/.gitignore +0 -0
  17. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/.python-version +0 -0
  18. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/README.md +0 -0
  19. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/main.py +0 -0
  20. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/scripts/generate_models.py +0 -0
  21. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/scripts/generate_runtime_packages.py +0 -0
  22. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/scripts/generate_sdk_completions.py +0 -0
  23. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/__init__.py +0 -0
  24. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/__init__.py +0 -0
  25. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/contract.py +0 -0
  26. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/preview.py +0 -0
  27. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/runner.py +0 -0
  28. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/sdk.py +0 -0
  29. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/session.py +0 -0
  30. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/config.py +0 -0
  31. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/__init__.py +0 -0
  32. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/base.py +0 -0
  33. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/broken_links/__init__.py +0 -0
  34. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/broken_links/detector.py +0 -0
  35. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/config.py +0 -0
  36. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/content/__init__.py +0 -0
  37. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/__init__.py +0 -0
  38. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/detector.py +0 -0
  39. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/extractor.py +0 -0
  40. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/__init__.py +0 -0
  41. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_base.py +0 -0
  42. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_custom_detector.py +0 -0
  43. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_decision.py +0 -0
  44. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_factory.py +0 -0
  45. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_gliner2.py +0 -0
  46. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_image_classification.py +0 -0
  47. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_llm.py +0 -0
  48. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_object_detection.py +0 -0
  49. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_provider_call.py +0 -0
  50. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_regex.py +0 -0
  51. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_tag.py +0 -0
  52. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_text_classification.py +0 -0
  53. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/trainer.py +0 -0
  54. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/__init__.py +0 -0
  55. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/contract.py +0 -0
  56. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/files.py +0 -0
  57. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/preview.py +0 -0
  58. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/results.py +0 -0
  59. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/runner.py +0 -0
  60. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/sdk.py +0 -0
  61. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/session.py +0 -0
  62. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/dependencies.py +0 -0
  63. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/engine_version.py +0 -0
  64. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/errors.py +0 -0
  65. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/pii/__init__.py +0 -0
  66. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/pii/detector.py +0 -0
  67. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/secrets/__init__.py +0 -0
  68. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/secrets/detector.py +0 -0
  69. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/threat/__init__.py +0 -0
  70. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/threat/code_security_detector.py +0 -0
  71. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/threat/yara_detector.py +0 -0
  72. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/file_evaluation/__init__.py +0 -0
  73. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/file_evaluation/asset_fixture.py +0 -0
  74. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/file_evaluation/runner.py +0 -0
  75. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/graph/__init__.py +0 -0
  76. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/graph/edges.py +0 -0
  77. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/models/generated_detectors.py +0 -0
  78. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/models/generated_input.py +0 -0
  79. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/models/generated_single_asset_scan_results.py +0 -0
  80. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/__init__.py +0 -0
  81. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/child.py +0 -0
  82. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/cli.py +0 -0
  83. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/cohort.py +0 -0
  84. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/contract.py +0 -0
  85. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/execute.py +0 -0
  86. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/files.py +0 -0
  87. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/groups.py +0 -0
  88. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/outputs.py +0 -0
  89. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/packages.py +0 -0
  90. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/protocol.py +0 -0
  91. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/redact.py +0 -0
  92. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/scaffold.py +0 -0
  93. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/sdk.py +0 -0
  94. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/serialize.py +0 -0
  95. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/__init__.py +0 -0
  96. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/base.py +0 -0
  97. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/console.py +0 -0
  98. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/factory.py +0 -0
  99. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/file.py +0 -0
  100. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/__init__.py +0 -0
  101. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/asset_payload_server.py +0 -0
  102. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/content_provider.py +0 -0
  103. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/detector_breaker.py +0 -0
  104. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/parsed_content_provider.py +0 -0
  105. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/payload_window.py +0 -0
  106. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/worker_pool.py +0 -0
  107. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/__init__.py +0 -0
  108. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/asset_metadata.py +0 -0
  109. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/atlassian_common.py +0 -0
  110. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/azure_blob_storage/__init__.py +0 -0
  111. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/azure_blob_storage/source.py +0 -0
  112. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/base.py +0 -0
  113. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/confluence/__init__.py +0 -0
  114. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/confluence/source.py +0 -0
  115. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/custom/__init__.py +0 -0
  116. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/custom/env.py +0 -0
  117. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/custom/runner.py +0 -0
  118. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/custom/source.py +0 -0
  119. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/databricks/__init__.py +0 -0
  120. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/databricks/source.py +0 -0
  121. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/delta_lake/__init__.py +0 -0
  122. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/delta_lake/source.py +0 -0
  123. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dependencies.py +0 -0
  124. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dremio/__init__.py +0 -0
  125. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dremio/client.py +0 -0
  126. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dremio/source.py +0 -0
  127. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dropbox/__init__.py +0 -0
  128. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dropbox/auth.py +0 -0
  129. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dropbox/source.py +0 -0
  130. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/elasticsearch/__init__.py +0 -0
  131. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/elasticsearch/source.py +0 -0
  132. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/email/__init__.py +0 -0
  133. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/email/source.py +0 -0
  134. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/git/__init__.py +0 -0
  135. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/git/source.py +0 -0
  136. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/google_cloud_storage/__init__.py +0 -0
  137. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/google_cloud_storage/source.py +0 -0
  138. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/google_workspace/__init__.py +0 -0
  139. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/google_workspace/source.py +0 -0
  140. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/hive/__init__.py +0 -0
  141. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/hive/source.py +0 -0
  142. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/hugging_face/__init__.py +0 -0
  143. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/hugging_face/source.py +0 -0
  144. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/iceberg/__init__.py +0 -0
  145. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/iceberg/source.py +0 -0
  146. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/jira/__init__.py +0 -0
  147. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/jira/source.py +0 -0
  148. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/kafka/__init__.py +0 -0
  149. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/kafka/rest.py +0 -0
  150. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/kafka/source.py +0 -0
  151. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/lakehouse_base.py +0 -0
  152. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/local_folder/__init__.py +0 -0
  153. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/local_folder/source.py +0 -0
  154. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/meilisearch/__init__.py +0 -0
  155. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/meilisearch/source.py +0 -0
  156. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/microsoft_365/__init__.py +0 -0
  157. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/microsoft_365/source.py +0 -0
  158. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mongodb/__init__.py +0 -0
  159. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mongodb/source.py +0 -0
  160. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mssql/__init__.py +0 -0
  161. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mssql/source.py +0 -0
  162. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mysql/__init__.py +0 -0
  163. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mysql/source.py +0 -0
  164. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/neo4j/__init__.py +0 -0
  165. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/neo4j/source.py +0 -0
  166. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/notion/__init__.py +0 -0
  167. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/notion/client.py +0 -0
  168. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/notion/source.py +0 -0
  169. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/object_storage/base.py +0 -0
  170. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/opensearch/__init__.py +0 -0
  171. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/opensearch/source.py +0 -0
  172. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/oracle/__init__.py +0 -0
  173. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/oracle/source.py +0 -0
  174. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/postgresql/__init__.py +0 -0
  175. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/postgresql/source.py +0 -0
  176. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/powerbi/__init__.py +0 -0
  177. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/powerbi/source.py +0 -0
  178. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/recipe_normalizer.py +0 -0
  179. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/reddit/__init__.py +0 -0
  180. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/reddit/source.py +0 -0
  181. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/s3_client.py +0 -0
  182. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/s3_compatible_storage/README.md +0 -0
  183. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/s3_compatible_storage/__init__.py +0 -0
  184. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/s3_compatible_storage/source.py +0 -0
  185. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/sandbox/__init__.py +0 -0
  186. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/sandbox/source.py +0 -0
  187. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/search_engine_base.py +0 -0
  188. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/servicedesk/__init__.py +0 -0
  189. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/servicedesk/source.py +0 -0
  190. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/slack/__init__.py +0 -0
  191. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/slack/source.py +0 -0
  192. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/snowflake/__init__.py +0 -0
  193. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/snowflake/source.py +0 -0
  194. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/sqlite/__init__.py +0 -0
  195. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/sqlite/source.py +0 -0
  196. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/tableau/__init__.py +0 -0
  197. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/tableau/source.py +0 -0
  198. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/tabular_base.py +0 -0
  199. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/tabular_utils.py +0 -0
  200. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/wordpress/__init__.py +0 -0
  201. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/wordpress/source.py +0 -0
  202. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/youtube/__init__.py +0 -0
  203. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/youtube/source.py +0 -0
  204. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/telemetry.py +0 -0
  205. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/__init__.py +0 -0
  206. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/archive_extraction.py +0 -0
  207. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/content_extraction.py +0 -0
  208. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/dependency_groups.py +0 -0
  209. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/embedded_files.py +0 -0
  210. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/external_video.py +0 -0
  211. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/file_metadata.py +0 -0
  212. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/file_parser.py +0 -0
  213. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/file_to_images.py +0 -0
  214. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/hashing.py +0 -0
  215. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/legacy_office.py +0 -0
  216. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/payload.py +0 -0
  217. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/range_reader.py +0 -0
  218. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/redaction.py +0 -0
  219. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/resources.py +0 -0
  220. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/sampling_cursor.py +0 -0
  221. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/sampling_frontier.py +0 -0
  222. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/source_files.py +0 -0
  223. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/sql_lineage.py +0 -0
  224. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/torch_runtime.py +0 -0
  225. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/transcription.py +0 -0
  226. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/urn.py +0 -0
  227. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/uv_sync.py +0 -0
  228. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/validation.py +0 -0
  229. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/video_processing.py +0 -0
  230. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/__init__.py +0 -0
  231. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/_lakehouse_fakes.py +0 -0
  232. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/conftest.py +0 -0
  233. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/__init__.py +0 -0
  234. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  235. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/conftest.py +0 -0
  236. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/content/__init__.py +0 -0
  237. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/__init__.py +0 -0
  238. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/conftest.py +0 -0
  239. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_decision_runner.py +0 -0
  240. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  241. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  242. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_llm_runner.py +0 -0
  243. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  244. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_regex_runner.py +0 -0
  245. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_runner_hardening.py +0 -0
  246. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_transformer_runners.py +0 -0
  247. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/__init__.py +0 -0
  248. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/conftest.py +0 -0
  249. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/sample_invoice.pdf +0 -0
  250. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_chunking.py +0 -0
  251. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_detector.py +0 -0
  252. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  253. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  254. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_severity.py +0 -0
  255. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/secrets/__init__.py +0 -0
  256. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  257. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  258. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_base_detector.py +0 -0
  259. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  260. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_custom_detector_session.py +0 -0
  261. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  262. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_detector_pipeline_types.py +0 -0
  263. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_detector_schema_examples.py +0 -0
  264. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_detector_types.py +0 -0
  265. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_phase2_detectors.py +0 -0
  266. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_registry.py +0 -0
  267. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_tag_runner.py +0 -0
  268. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/threat/__init__.py +0 -0
  269. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/threat/test_code_security_detector.py +0 -0
  270. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/threat/test_yara_detector.py +0 -0
  271. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  272. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/integration/test_wordpress_links_assets.py +0 -0
  273. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/pipeline/test_detector_breaker.py +0 -0
  274. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/pipeline/test_detector_outcomes.py +0 -0
  275. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/pipeline/test_detector_pipeline.py +0 -0
  276. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/pipeline/test_worker_pool.py +0 -0
  277. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_arrow_ipc_payloads.py +0 -0
  278. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_assets_metadata_catalog.py +0 -0
  279. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_contract.py +0 -0
  280. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_edges.py +0 -0
  281. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_patch.py +0 -0
  282. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_scan_cache.py +0 -0
  283. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_sdk.py +0 -0
  284. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_session.py +0 -0
  285. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_tags_end_to_end.py +0 -0
  286. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_automatic_new_first.py +0 -0
  287. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_azure_blob_storage_source.py +0 -0
  288. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_base_source_attachment.py +0 -0
  289. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_base_source_sampling.py +0 -0
  290. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_byte_valued_fields.py +0 -0
  291. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_config.py +0 -0
  292. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_confluence_source.py +0 -0
  293. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_detector_end_to_end.py +0 -0
  294. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_detector_evaluation.py +0 -0
  295. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_detector_notebook.py +0 -0
  296. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_detector_templates.py +0 -0
  297. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_extractor.py +0 -0
  298. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_source.py +0 -0
  299. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_source_relationships.py +0 -0
  300. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_source_tags_end_to_end.py +0 -0
  301. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_databricks_source.py +0 -0
  302. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_delta_lake_source.py +0 -0
  303. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_dependency_groups.py +0 -0
  304. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_detector_scope.py +0 -0
  305. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_dremio_source.py +0 -0
  306. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_dropbox_auth_command.py +0 -0
  307. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_dropbox_source.py +0 -0
  308. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_elasticsearch_source.py +0 -0
  309. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_email_source.py +0 -0
  310. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_file_evaluation_runner.py +0 -0
  311. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_git_source.py +0 -0
  312. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_google_cloud_storage_source.py +0 -0
  313. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_google_workspace_source.py +0 -0
  314. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_graph_edges.py +0 -0
  315. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_hashing.py +0 -0
  316. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_hive_source.py +0 -0
  317. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_hugging_face_source.py +0 -0
  318. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_iceberg_source.py +0 -0
  319. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_jira_source.py +0 -0
  320. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_kafka_source.py +0 -0
  321. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_lineage_loss_reporting.py +0 -0
  322. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_local_folder_source.py +0 -0
  323. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_main.py +0 -0
  324. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_meilisearch_source.py +0 -0
  325. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_microsoft_365_source.py +0 -0
  326. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_mongodb_source.py +0 -0
  327. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_mssql_source.py +0 -0
  328. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_mysql_source.py +0 -0
  329. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_neo4j_source.py +0 -0
  330. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_asset_entity.py +0 -0
  331. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_asset_extract.py +0 -0
  332. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_asset_tags.py +0 -0
  333. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_cohort.py +0 -0
  334. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_execute.py +0 -0
  335. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_files.py +0 -0
  336. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_groups.py +0 -0
  337. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_log_levels.py +0 -0
  338. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_packages.py +0 -0
  339. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_query_assets.py +0 -0
  340. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_sdk_exports.py +0 -0
  341. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_serialize.py +0 -0
  342. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_templates.py +0 -0
  343. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notion_source.py +0 -0
  344. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_object_storage_download_efficiency.py +0 -0
  345. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_opensearch_source.py +0 -0
  346. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_oracle_source.py +0 -0
  347. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_outputs.py +0 -0
  348. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_payload_window.py +0 -0
  349. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_payload_window_parquet.py +0 -0
  350. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_postgresql_source.py +0 -0
  351. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_powerbi_source.py +0 -0
  352. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_range_reading.py +0 -0
  353. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_recipe_normalizer.py +0 -0
  354. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_reddit_source.py +0 -0
  355. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_rest_backpressure.py +0 -0
  356. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_runtime_packages.py +0 -0
  357. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_s3_compatible_storage_source.py +0 -0
  358. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sampling_automatic.py +0 -0
  359. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sampling_cursor.py +0 -0
  360. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sampling_frontier.py +0 -0
  361. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sandbox_source.py +0 -0
  362. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_scan_cache.py +0 -0
  363. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sdk_completions.py +0 -0
  364. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_secret_redaction.py +0 -0
  365. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_semantic_declarations.py +0 -0
  366. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_servicedesk_source.py +0 -0
  367. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_slack_source.py +0 -0
  368. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_snowflake_source.py +0 -0
  369. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_source_dependency_groups.py +0 -0
  370. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_source_strategy_conformance.py +0 -0
  371. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sqlite_source.py +0 -0
  372. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_tableau_source.py +0 -0
  373. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_tabular_automatic_sampling.py +0 -0
  374. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_tabular_lineage.py +0 -0
  375. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_tabular_utils.py +0 -0
  376. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_torch_runtime.py +0 -0
  377. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_urn.py +0 -0
  378. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_uv_sync.py +0 -0
  379. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_wordpress_source.py +0 -0
  380. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_youtube_source.py +0 -0
  381. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_youtube_source_integration.py +0 -0
  382. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_archive_extraction.py +0 -0
  383. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_content_extraction.py +0 -0
  384. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_embedded_files.py +0 -0
  385. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_file_metadata.py +0 -0
  386. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_file_parser.py +0 -0
  387. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_file_to_images.py +0 -0
  388. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_legacy_office.py +0 -0
  389. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_payload.py +0 -0
  390. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_transcription.py +0 -0
  391. {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_video_processing.py +0 -0
@@ -0,0 +1,3 @@
1
+ $ uv sync
2
+ Resolved 324 packages in 342ms
3
+ Checked 51 packages in 4ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: classifyre-cli
3
- Version: 0.6.11
3
+ Version: 0.6.12
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.6.11",
3
+ "version": "0.6.12",
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.6.11"
3
+ version = "0.6.12"
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
@@ -553,6 +553,24 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
553
553
  await augmentation.startup()
554
554
 
555
555
  scan_cache = ScanCache(recipe, source, payload_windows=payload_windows)
556
+
557
+ # What the workspace embeds besides text follows its
558
+ # embedding model, so the API says and the run asks once.
559
+ # The pipeline prepares that media; the scan cache uses the
560
+ # same plan to know which unchanged assets still owe theirs.
561
+ if hasattr(sink, "fetch_embedding_plan"):
562
+ from .pipeline.media_artifact import EmbeddingPlan
563
+
564
+ embedding_plan = EmbeddingPlan.from_payload(
565
+ await sink.fetch_embedding_plan()
566
+ )
567
+ if embedding_plan.collects_media:
568
+ logger.info(
569
+ "Preparing media for embedding: %s",
570
+ ", ".join(sorted(embedding_plan.media)),
571
+ )
572
+ pipeline.set_embedding_plan(embedding_plan)
573
+ scan_cache.set_embedding_plan(embedding_plan)
556
574
  if scan_cache.enabled:
557
575
  logger.info(
558
576
  "Scan cache active (verify=%s): unchanged assets and"
@@ -675,7 +693,13 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
675
693
  source.evict_asset_cache(asset_hash)
676
694
  return
677
695
 
678
- if plan.mode == "partial":
696
+ if plan.media_only:
697
+ logger.info(
698
+ "Cached %s, but %s",
699
+ asset.name,
700
+ plan.reason,
701
+ )
702
+ elif plan.mode == "partial":
679
703
  logger.info(
680
704
  "Partially cached %s: running %s",
681
705
  asset.name,
@@ -761,6 +785,13 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
761
785
  f_total, f_by_sev, f_by_det = _compute_findings_counts(
762
786
  result.findings or []
763
787
  )
788
+ if plan.media_only and plan.prior is not None:
789
+ # Read again only to send its picture or
790
+ # recording: no detector ran, so the counts
791
+ # of its last real scan are still its counts.
792
+ f_total = plan.prior.findings_total or 0
793
+ f_by_sev = plan.prior.findings_by_severity or {}
794
+ f_by_det = plan.prior.findings_by_detector or {}
764
795
  payload = _asset_to_payload(result)
765
796
 
766
797
  content_hash = plan.content_hash
@@ -785,6 +816,7 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
785
816
  findings_total=f_total,
786
817
  findings_by_severity=f_by_sev,
787
818
  findings_by_detector=f_by_det,
819
+ media_state=scan_cache.media_state_for(asset),
788
820
  )
789
821
  if state is not None:
790
822
  payload["scan_cache"] = state
@@ -347,13 +347,29 @@ class RestOutputSink:
347
347
  )
348
348
 
349
349
  async def emit_text_chunks(self, asset_hash: str, artifact: TextArtifact) -> None:
350
- """Send extracted text provenance; the API embeds it asynchronously."""
350
+ """Send extracted text provenance; the API embeds it asynchronously.
351
+
352
+ Prepared pictures and clips ride in the same request, because the API
353
+ replaces everything it holds for the asset in one step.
354
+ """
351
355
  source_id = self._require_source_id()
352
- self._request_json(
353
- "POST",
354
- f"/sources/{source_id}/embeddings/chunks",
355
- {"assetHash": asset_hash, "chunks": artifact.chunks},
356
- )
356
+ body: dict[str, Any] = {"assetHash": asset_hash, "chunks": artifact.chunks}
357
+ if artifact.media:
358
+ body["media"] = artifact.media
359
+ self._request_json("POST", f"/sources/{source_id}/embeddings/chunks", body)
360
+
361
+ async def fetch_embedding_plan(self) -> dict[str, Any]:
362
+ """What this workspace wants prepared for embedding besides text.
363
+
364
+ Asked once per run. An API that predates the endpoint answers 404, and
365
+ any failure here means the same thing: prepare no media, and embed the
366
+ text as every scan before this did.
367
+ """
368
+ try:
369
+ return self._request_json("GET", "/embeddings/collection")
370
+ except Exception as exc:
371
+ logger.info("No embedding collection plan (%s); embedding text only", exc)
372
+ return {}
357
373
 
358
374
  def _split_by_size(self, assets: list[dict[str, Any]]) -> list[list[dict[str, Any]]]:
359
375
  """Split assets into chunks that each stay under _MAX_BATCH_BYTES."""
@@ -29,6 +29,8 @@ from ..utils.file_parser import TextExtractionCoverageError, resolve_mime_type
29
29
  from ..utils.redaction import Redactor, redact_generic, short_id_for_log
30
30
  from .content_provider import ContentProvider
31
31
  from .detector_breaker import DetectorBreaker
32
+ from .finding_context import attach_context
33
+ from .media_artifact import EmbeddingPlan, prepare_media
32
34
  from .text_artifact import TextArtifact
33
35
  from .worker_pool import DetectorWorkerPool, is_io_bound_detector
34
36
 
@@ -151,12 +153,60 @@ class DetectorPipeline:
151
153
  # disabled here is disabled for this run and nothing longer.
152
154
  self._breaker = DetectorBreaker()
153
155
  self._text_artifacts: dict[str, TextArtifact] = {}
156
+ # What the workspace wants embedded besides text. Empty until the run
157
+ # asks the API, and empty for good on a text-only embedding model.
158
+ self._embedding_plan = EmbeddingPlan()
154
159
  self._text_extraction_errors: dict[str, list[str]] = {}
155
160
  self._text_extraction_statuses: dict[str, TextExtractionStatus] = {}
156
161
 
157
162
  def take_text_artifact(self, asset_hash: str) -> TextArtifact | None:
158
163
  return self._text_artifacts.pop(asset_hash, None)
159
164
 
165
+ def set_embedding_plan(self, plan: EmbeddingPlan) -> None:
166
+ """Say which media, if any, this run prepares for embedding."""
167
+ self._embedding_plan = plan
168
+
169
+ async def _collect_media(self, asset: SingleAssetScanResults, artifact: TextArtifact) -> None:
170
+ """Attach the asset's own picture or recording, prepared for embedding.
171
+
172
+ Only for an asset that IS a picture or a recording, and only when the
173
+ workspace's model embeds that kind of content. Best-effort throughout:
174
+ media adds a way to find the asset, and failing to prepare it leaves
175
+ the asset exactly as findable as it was before this existed.
176
+ """
177
+ spec = self._embedding_plan.spec_for(asset.asset_type)
178
+ if spec is None:
179
+ return
180
+ for candidate in (asset.external_url, asset.hash):
181
+ asset_id = str(candidate or "").strip()
182
+ if not asset_id:
183
+ continue
184
+ try:
185
+ fetched = await self.content_provider.fetch_bytes(asset_id)
186
+ except Exception as exc:
187
+ logger.warning(
188
+ "Could not read %s to prepare its %s for embedding: %s",
189
+ asset.name,
190
+ spec.modality,
191
+ redact_generic(self._redactor.redact(str(exc))),
192
+ )
193
+ return
194
+ if not fetched or not fetched[0]:
195
+ continue
196
+ # Decoding and re-encoding is CPU work; keep it off the event loop
197
+ # the rest of the scan is running on.
198
+ items = await asyncio.to_thread(prepare_media, fetched[0], spec)
199
+ for mime_type, data in items:
200
+ artifact.add_media(spec.modality, mime_type, data)
201
+ if items:
202
+ logger.info(
203
+ "Prepared %d %s item(s) of %s for embedding",
204
+ len(items),
205
+ spec.modality,
206
+ asset.name,
207
+ )
208
+ return
209
+
160
210
  def _register_detector_info(self, detector: BaseDetector, info: _DetectorInfo) -> None:
161
211
  self._detector_info[id(detector)] = info
162
212
 
@@ -401,6 +451,9 @@ class DetectorPipeline:
401
451
  scan_warnings.extend(code_warnings)
402
452
  detector_types_run = self._merge_detector_types(detector_types_run, code_types_run)
403
453
 
454
+ if not records_only:
455
+ await self._collect_media(asset, artifact)
456
+
404
457
  scan_duration = int((datetime.now(UTC) - scan_started).total_seconds() * 1000)
405
458
 
406
459
  asset.findings = findings
@@ -1011,6 +1064,8 @@ class DetectorPipeline:
1011
1064
  }
1012
1065
  )
1013
1066
  detector_findings.append(finding_with_meta)
1067
+ if isinstance(content, str):
1068
+ detector_findings = attach_context(detector_findings, content)
1014
1069
 
1015
1070
  tripped = self._breaker.record(
1016
1071
  breaker_key,
@@ -0,0 +1,99 @@
1
+ """The text around a match.
2
+
3
+ A finding is a value and where it was found. What the value means is in the
4
+ words either side of it: "IBAN DE89..." in a payment reminder and the same
5
+ number in a list of test fixtures are two different pieces of evidence, and
6
+ nothing downstream can tell them apart from the value alone. The API ranks a
7
+ finding by its surrounding text, embeds the match together with it, and shows
8
+ it to the reviewer next to the match.
9
+
10
+ No detector returns that text: each answers with the value and, at most, a
11
+ position. So it is cut here, once, from the content the detectors were given,
12
+ for every finding that does not already carry it.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+
19
+ from ..models.generated_single_asset_scan_results import DetectionResult
20
+
21
+ # Characters kept on each side. The ranking treats 320 characters of evidence
22
+ # as a full context, so a match of ordinary length with both sides reaches it.
23
+ CONTEXT_CHARS = 160
24
+
25
+ # A match longer than this is already a passage (an LLM detector quoting a
26
+ # paragraph); text around it would be the next paragraph, not its context.
27
+ MAX_MATCH_CHARS = 600
28
+
29
+ _CHAR_RANGE = re.compile(r"character range (\d+)-(\d+)")
30
+
31
+
32
+ def _locate(content: str, matched: str, finding: DetectionResult, start_at: int) -> int:
33
+ """Offset of this finding's match in `content`, or -1."""
34
+ description = getattr(finding.location, "description", None) if finding.location else None
35
+ stated = _CHAR_RANGE.search(description) if isinstance(description, str) else None
36
+ if stated:
37
+ start, end = int(stated.group(1)), int(stated.group(2))
38
+ if content[start:end] == matched:
39
+ return start
40
+ found = content.find(matched, start_at)
41
+ return found if found >= 0 else content.find(matched)
42
+
43
+
44
+ def _before(content: str, start: int) -> str | None:
45
+ cut = max(0, start - CONTEXT_CHARS)
46
+ text = content[cut:start]
47
+ # A window that opens in the middle of a word starts with half of it.
48
+ if cut > 0 and not content[cut - 1].isspace() and text[:1].strip():
49
+ parts = text.split(None, 1)
50
+ text = parts[1] if len(parts) > 1 else ""
51
+ return " ".join(text.split()) or None
52
+
53
+
54
+ def _after(content: str, end: int) -> str | None:
55
+ cut = min(len(content), end + CONTEXT_CHARS)
56
+ text = content[end:cut]
57
+ if cut < len(content) and not content[cut].isspace() and text[-1:].strip():
58
+ parts = text.rsplit(None, 1)
59
+ text = parts[0] if len(parts) > 1 else ""
60
+ return " ".join(text.split()) or None
61
+
62
+
63
+ def attach_context(findings: list[DetectionResult], content: str) -> list[DetectionResult]:
64
+ """`findings` with the text around each match filled in where it was missing.
65
+
66
+ A value that occurs several times is matched to its occurrences in order,
67
+ so two findings of one value do not both get the first one's surroundings.
68
+ A match that cannot be found in the content (a detector that reports a
69
+ normalised value, or a verdict rather than a quotation) is left as it is.
70
+ """
71
+ if not content:
72
+ return findings
73
+ cursor: dict[str, int] = {}
74
+ out: list[DetectionResult] = []
75
+ for finding in findings:
76
+ matched = finding.matched_content
77
+ if (
78
+ finding.context_before
79
+ or finding.context_after
80
+ or not matched
81
+ or len(matched) > MAX_MATCH_CHARS
82
+ ):
83
+ out.append(finding)
84
+ continue
85
+ start = _locate(content, matched, finding, cursor.get(matched, 0))
86
+ if start < 0:
87
+ out.append(finding)
88
+ continue
89
+ end = start + len(matched)
90
+ cursor[matched] = end
91
+ out.append(
92
+ finding.model_copy(
93
+ update={
94
+ "context_before": _before(content, start),
95
+ "context_after": _after(content, end),
96
+ }
97
+ )
98
+ )
99
+ return out
@@ -0,0 +1,262 @@
1
+ """Pictures and sound prepared for embedding as what they are.
2
+
3
+ Text read out of an asset (OCR, transcription) is embedded as text. A model
4
+ that also embeds pictures or sound can find an asset by what it shows or how it
5
+ sounds, which is the only way to find a photograph with no writing in it. This
6
+ module prepares that media the way the API asks for it: one small JPEG per
7
+ picture, mono 16 kHz WAV clips per recording. The API stores the prepared copy,
8
+ because a later rebuild re-embeds from it and the scan will not send it again.
9
+
10
+ What to prepare is the API's decision (``GET /embeddings/collection``), since
11
+ it follows the workspace's embedding model. With nothing asked for, nothing
12
+ here runs.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import io
18
+ import logging
19
+ import wave
20
+ from dataclasses import dataclass, field
21
+ from typing import Any
22
+
23
+ from ..detectors.dependencies import MissingDependencyError, require_module
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # uv groups that carry Pillow, and PyAV (which arrives with faster-whisper). A
28
+ # picture or a recording has normally been through OCR or transcription by the
29
+ # time it gets here, so these are already installed; a scan that somehow is
30
+ # not installs the first one on demand.
31
+ _IMAGE_UV_GROUPS = ["file-processing", "content", "llm", "custom"]
32
+ _AUDIO_UV_GROUPS = ["transcription"]
33
+
34
+ # Scan-cache key under which an asset's state records that its media was
35
+ # collected. Lives beside the detector fingerprints because it answers the same
36
+ # question for the same reason: was this work done for this asset under the
37
+ # rules in force now. The API names the key with the plan (it is the one that
38
+ # takes the record back when the media is deleted); this is the same name, for
39
+ # an API that does not send one.
40
+ MEDIA_CACHE_KEY_PREFIX = "EMBED_MEDIA::"
41
+
42
+ # Shortest clip worth sending: of a whole recording, and of what is left over
43
+ # after the full clips of a longer one.
44
+ _MIN_CLIP_SECONDS = 1
45
+ _MIN_TAIL_SECONDS = 5
46
+
47
+ # Asset types whose own bytes are a picture or a recording.
48
+ _MODALITY_BY_ASSET_TYPE = {"IMAGE": "image", "AUDIO": "audio"}
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class MediaSpec:
53
+ """How one kind of media is prepared."""
54
+
55
+ modality: str
56
+ mime_type: str
57
+ max_items: int = 1
58
+ max_edge: int = 768
59
+ quality: int = 85
60
+ sample_rate: int = 16_000
61
+ clip_seconds: int = 30
62
+ cache_key: str = ""
63
+ # Changes when the way this kind of media is prepared changes.
64
+ fingerprint: str = ""
65
+
66
+ @classmethod
67
+ def from_payload(cls, raw: Any) -> MediaSpec | None:
68
+ if not isinstance(raw, dict):
69
+ return None
70
+ modality = str(raw.get("modality") or "").strip().lower()
71
+ if modality not in {"image", "audio"}:
72
+ return None
73
+
74
+ def number(key: str, default: int, minimum: int) -> int:
75
+ value = raw.get(key)
76
+ return max(minimum, int(value)) if isinstance(value, int | float) else default
77
+
78
+ return cls(
79
+ modality=modality,
80
+ mime_type=str(
81
+ raw.get("mimeType") or ("image/jpeg" if modality == "image" else "audio/wav")
82
+ ),
83
+ max_items=number("maxItems", 1, 1),
84
+ max_edge=number("maxEdge", 768, 32),
85
+ quality=min(100, number("quality", 85, 1)),
86
+ sample_rate=number("sampleRate", 16_000, 8_000),
87
+ clip_seconds=number("clipSeconds", 30, 1),
88
+ cache_key=str(raw.get("cacheKey") or f"{MEDIA_CACHE_KEY_PREFIX}{modality}"),
89
+ fingerprint=str(raw.get("fingerprint") or ""),
90
+ )
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class EmbeddingPlan:
95
+ """What this workspace wants sent for embedding."""
96
+
97
+ media: dict[str, MediaSpec] = field(default_factory=dict)
98
+
99
+ @classmethod
100
+ def from_payload(cls, raw: Any) -> EmbeddingPlan:
101
+ if not isinstance(raw, dict):
102
+ return cls()
103
+ specs: dict[str, MediaSpec] = {}
104
+ entries = raw.get("media")
105
+ for entry in entries if isinstance(entries, list) else []:
106
+ spec = MediaSpec.from_payload(entry)
107
+ if spec is not None:
108
+ specs[spec.modality] = spec
109
+ return cls(media=specs)
110
+
111
+ @property
112
+ def collects_media(self) -> bool:
113
+ return bool(self.media)
114
+
115
+ def spec_for(self, asset_type: Any) -> MediaSpec | None:
116
+ """The spec for an asset whose own bytes are media, if it is collected."""
117
+ name = str(getattr(asset_type, "value", asset_type) or "").upper()
118
+ modality = _MODALITY_BY_ASSET_TYPE.get(name)
119
+ return self.media.get(modality) if modality else None
120
+
121
+ def cache_entry_for(self, asset_type: Any) -> tuple[str, str] | None:
122
+ """Scan-cache (key, fingerprint) proving this asset's media was sent.
123
+
124
+ Per kind of media, so that starting to embed sound does not make every
125
+ picture read as not yet sent.
126
+ """
127
+ spec = self.spec_for(asset_type)
128
+ if spec is None:
129
+ return None
130
+ key = spec.cache_key or f"{MEDIA_CACHE_KEY_PREFIX}{spec.modality}"
131
+ return key, spec.fingerprint or "1"
132
+
133
+
134
+ def prepare_image(raw: bytes, spec: MediaSpec) -> bytes | None:
135
+ """Re-encode a picture as an RGB JPEG no larger than ``spec.max_edge``.
136
+
137
+ Vision encoders work at a few hundred pixels a side, so a larger copy is
138
+ only storage. Returns None when the bytes are not a picture Pillow can
139
+ read, or Pillow is not available; the asset is then embedded by its text
140
+ alone, as it would have been anyway.
141
+ """
142
+ if not raw:
143
+ return None
144
+ try:
145
+ pil = require_module("PIL.Image", "media embedding", _IMAGE_UV_GROUPS)
146
+ ops = require_module("PIL.ImageOps", "media embedding", _IMAGE_UV_GROUPS)
147
+ except MissingDependencyError as exc:
148
+ logger.warning("Cannot prepare a picture for embedding: %s", exc)
149
+ return None
150
+ try:
151
+ # HEIC (every recent phone photograph) is read through a plug-in that
152
+ # has to be registered; without it Pillow does not know the format.
153
+ try:
154
+ import pillow_heif # type: ignore[import-not-found]
155
+
156
+ pillow_heif.register_heif_opener()
157
+ except Exception:
158
+ pass
159
+ with pil.open(io.BytesIO(raw)) as image:
160
+ # A phone photograph is stored sideways with a tag saying which way
161
+ # is up; the encoder only sees pixels.
162
+ upright = ops.exif_transpose(image) or image
163
+ # Flatten transparency onto white: JPEG has no alpha, and dropping
164
+ # the channel turns a transparent background black.
165
+ if upright.mode in {"RGBA", "LA"} or "transparency" in upright.info:
166
+ rgba = upright.convert("RGBA")
167
+ canvas = pil.new("RGB", rgba.size, (255, 255, 255))
168
+ canvas.paste(rgba, mask=rgba.getchannel("A"))
169
+ rgb = canvas
170
+ else:
171
+ rgb = upright.convert("RGB")
172
+ rgb.thumbnail((spec.max_edge, spec.max_edge), pil.Resampling.LANCZOS)
173
+ buffer = io.BytesIO()
174
+ rgb.save(buffer, format="JPEG", quality=spec.quality, optimize=True)
175
+ return buffer.getvalue()
176
+ except Exception as exc:
177
+ logger.warning("Could not prepare a picture for embedding: %s", exc)
178
+ return None
179
+
180
+
181
+ def _pcm_to_wav(pcm: bytes, sample_rate: int) -> bytes:
182
+ buffer = io.BytesIO()
183
+ with wave.open(buffer, "wb") as out:
184
+ out.setnchannels(1)
185
+ out.setsampwidth(2)
186
+ out.setframerate(sample_rate)
187
+ out.writeframes(pcm)
188
+ return buffer.getvalue()
189
+
190
+
191
+ def prepare_audio_clips(raw: bytes, spec: MediaSpec) -> list[bytes]:
192
+ """Decode a recording to mono 16-bit WAV clips at ``spec.sample_rate``.
193
+
194
+ At most ``spec.max_items`` clips of ``spec.clip_seconds`` each, from the
195
+ start: the opening minutes say what a recording is, and every clip is a
196
+ megabyte kept. Decoding stops once enough has been read, so a long file is
197
+ not decoded to its end. Returns [] when the bytes hold no audio PyAV can
198
+ read, or PyAV is not available.
199
+ """
200
+ if not raw:
201
+ return []
202
+ try:
203
+ pyav = require_module("av", "media embedding", _AUDIO_UV_GROUPS)
204
+ except MissingDependencyError as exc:
205
+ logger.warning("Cannot prepare a recording for embedding: %s", exc)
206
+ return []
207
+
208
+ clip_bytes = spec.sample_rate * spec.clip_seconds * 2
209
+ wanted = clip_bytes * spec.max_items
210
+ pcm = bytearray()
211
+ container = None
212
+ try:
213
+ container = pyav.open(io.BytesIO(raw), metadata_errors="ignore")
214
+ streams = [stream for stream in container.streams if stream.type == "audio"]
215
+ if not streams:
216
+ return []
217
+ resampler = pyav.audio.resampler.AudioResampler(
218
+ format="s16", layout="mono", rate=spec.sample_rate
219
+ )
220
+
221
+ def drain(frames: Any) -> None:
222
+ for frame in frames if isinstance(frames, list) else [frames]:
223
+ if frame is not None:
224
+ pcm.extend(bytes(frame.planes[0])[: frame.samples * 2])
225
+
226
+ for frame in container.decode(streams[0]):
227
+ drain(resampler.resample(frame))
228
+ if len(pcm) >= wanted:
229
+ break
230
+ else:
231
+ # Only a file read to its end has a resampler tail worth flushing.
232
+ try:
233
+ drain(resampler.resample(None))
234
+ except Exception:
235
+ pass
236
+ except Exception as exc:
237
+ logger.warning("Could not prepare a recording for embedding: %s", exc)
238
+ return []
239
+ finally:
240
+ if container is not None:
241
+ container.close()
242
+
243
+ clips: list[bytes] = []
244
+ for start in range(0, min(len(pcm), wanted), clip_bytes):
245
+ chunk = bytes(pcm[start : start + clip_bytes])
246
+ # A short recording is sent whole: a second of sound can be all there
247
+ # is. The tail of a long one is different, since the clips before it
248
+ # already say what the recording is and a few seconds more add a vector
249
+ # of their own for almost nothing.
250
+ leftover = bool(clips) and len(chunk) < clip_bytes
251
+ shortest = _MIN_TAIL_SECONDS if leftover else _MIN_CLIP_SECONDS
252
+ if len(chunk) >= spec.sample_rate * 2 * shortest:
253
+ clips.append(_pcm_to_wav(chunk, spec.sample_rate))
254
+ return clips
255
+
256
+
257
+ def prepare_media(raw: bytes, spec: MediaSpec) -> list[tuple[str, bytes]]:
258
+ """Prepared (mime type, bytes) items for one asset under ``spec``."""
259
+ if spec.modality == "image":
260
+ image = prepare_image(raw, spec)
261
+ return [(spec.mime_type, image)] if image else []
262
+ return [(spec.mime_type, clip) for clip in prepare_audio_clips(raw, spec)]