classifyre-cli 0.4.80__tar.gz → 0.4.82__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 (278) hide show
  1. classifyre_cli-0.4.82/.turbo/turbo-build.log +3 -0
  2. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/package.json +1 -1
  4. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/databricks/source.py +3 -1
  6. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/kafka/source.py +5 -4
  7. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mongodb/source.py +5 -1
  8. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/neo4j/source.py +4 -1
  9. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/oracle/source.py +5 -1
  10. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/snowflake/source.py +3 -1
  11. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/tabular_base.py +6 -1
  12. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/embedded_files.py +73 -27
  13. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/file_parser.py +74 -12
  14. classifyre_cli-0.4.82/tests/test_byte_valued_fields.py +141 -0
  15. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_embedded_files.py +101 -1
  16. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/uv.lock +21 -21
  17. classifyre_cli-0.4.80/.turbo/turbo-build.log +0 -3
  18. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/.gitignore +0 -0
  19. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/.python-version +0 -0
  20. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/README.md +0 -0
  21. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/main.py +0 -0
  22. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/scripts/generate_models.py +0 -0
  23. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/__init__.py +0 -0
  24. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/config.py +0 -0
  25. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/__init__.py +0 -0
  26. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/base.py +0 -0
  27. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/broken_links/__init__.py +0 -0
  28. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/broken_links/detector.py +0 -0
  29. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/config.py +0 -0
  30. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/content/__init__.py +0 -0
  31. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/__init__.py +0 -0
  32. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/detector.py +0 -0
  33. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/extractor.py +0 -0
  34. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/__init__.py +0 -0
  35. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_base.py +0 -0
  36. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_factory.py +0 -0
  37. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_gliner2.py +0 -0
  38. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_image_classification.py +0 -0
  39. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_llm.py +0 -0
  40. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_object_detection.py +0 -0
  41. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_regex.py +0 -0
  42. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_text_classification.py +0 -0
  43. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/trainer.py +0 -0
  44. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/dependencies.py +0 -0
  45. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/engine_version.py +0 -0
  46. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/pii/__init__.py +0 -0
  47. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/pii/detector.py +0 -0
  48. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/secrets/__init__.py +0 -0
  49. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/secrets/detector.py +0 -0
  50. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/threat/__init__.py +0 -0
  51. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/threat/code_security_detector.py +0 -0
  52. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/threat/yara_detector.py +0 -0
  53. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/file_evaluation/__init__.py +0 -0
  54. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/file_evaluation/runner.py +0 -0
  55. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/main.py +0 -0
  56. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/models/generated_detectors.py +0 -0
  57. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/models/generated_input.py +0 -0
  58. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/models/generated_single_asset_scan_results.py +0 -0
  59. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/__init__.py +0 -0
  60. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/base.py +0 -0
  61. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/console.py +0 -0
  62. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/factory.py +0 -0
  63. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/file.py +0 -0
  64. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/rest.py +0 -0
  65. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/__init__.py +0 -0
  66. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/content_provider.py +0 -0
  67. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/detector_pipeline.py +0 -0
  68. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/parsed_content_provider.py +0 -0
  69. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/payload_window.py +0 -0
  70. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/scan_cache.py +0 -0
  71. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/text_artifact.py +0 -0
  72. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/worker_pool.py +0 -0
  73. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/__init__.py +0 -0
  74. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/asset_metadata.py +0 -0
  75. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/atlassian_common.py +0 -0
  76. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/azure_blob_storage/__init__.py +0 -0
  77. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/azure_blob_storage/source.py +0 -0
  78. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/base.py +0 -0
  79. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/confluence/__init__.py +0 -0
  80. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/confluence/source.py +0 -0
  81. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/databricks/__init__.py +0 -0
  82. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/delta_lake/__init__.py +0 -0
  83. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/delta_lake/source.py +0 -0
  84. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/dependencies.py +0 -0
  85. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/dropbox/__init__.py +0 -0
  86. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/dropbox/auth.py +0 -0
  87. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/dropbox/source.py +0 -0
  88. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/elasticsearch/__init__.py +0 -0
  89. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/elasticsearch/source.py +0 -0
  90. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/email/__init__.py +0 -0
  91. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/email/source.py +0 -0
  92. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/google_cloud_storage/__init__.py +0 -0
  93. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/google_cloud_storage/source.py +0 -0
  94. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/google_workspace/__init__.py +0 -0
  95. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/google_workspace/source.py +0 -0
  96. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/hive/__init__.py +0 -0
  97. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/hive/source.py +0 -0
  98. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/hugging_face/__init__.py +0 -0
  99. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/hugging_face/source.py +0 -0
  100. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/iceberg/__init__.py +0 -0
  101. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/iceberg/source.py +0 -0
  102. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/jira/__init__.py +0 -0
  103. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/jira/source.py +0 -0
  104. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/kafka/__init__.py +0 -0
  105. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/kafka/rest.py +0 -0
  106. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/lakehouse_base.py +0 -0
  107. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/local_folder/__init__.py +0 -0
  108. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/local_folder/source.py +0 -0
  109. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/meilisearch/__init__.py +0 -0
  110. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/meilisearch/source.py +0 -0
  111. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/microsoft_365/__init__.py +0 -0
  112. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/microsoft_365/source.py +0 -0
  113. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mongodb/__init__.py +0 -0
  114. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mssql/__init__.py +0 -0
  115. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mssql/source.py +0 -0
  116. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mysql/__init__.py +0 -0
  117. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mysql/source.py +0 -0
  118. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/neo4j/__init__.py +0 -0
  119. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/notion/__init__.py +0 -0
  120. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/notion/client.py +0 -0
  121. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/notion/source.py +0 -0
  122. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/object_storage/base.py +0 -0
  123. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/opensearch/__init__.py +0 -0
  124. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/opensearch/source.py +0 -0
  125. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/oracle/__init__.py +0 -0
  126. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/postgresql/__init__.py +0 -0
  127. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/postgresql/source.py +0 -0
  128. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/powerbi/__init__.py +0 -0
  129. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/powerbi/source.py +0 -0
  130. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/recipe_normalizer.py +0 -0
  131. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/reddit/__init__.py +0 -0
  132. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/reddit/source.py +0 -0
  133. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/s3_client.py +0 -0
  134. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/s3_compatible_storage/README.md +0 -0
  135. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/s3_compatible_storage/__init__.py +0 -0
  136. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/s3_compatible_storage/source.py +0 -0
  137. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/sandbox/__init__.py +0 -0
  138. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/sandbox/source.py +0 -0
  139. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/search_engine_base.py +0 -0
  140. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/servicedesk/__init__.py +0 -0
  141. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/servicedesk/source.py +0 -0
  142. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/slack/__init__.py +0 -0
  143. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/slack/source.py +0 -0
  144. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/snowflake/__init__.py +0 -0
  145. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/sqlite/__init__.py +0 -0
  146. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/sqlite/source.py +0 -0
  147. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/tableau/__init__.py +0 -0
  148. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/tableau/source.py +0 -0
  149. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/tabular_utils.py +0 -0
  150. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/wordpress/__init__.py +0 -0
  151. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/wordpress/source.py +0 -0
  152. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/youtube/__init__.py +0 -0
  153. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/youtube/source.py +0 -0
  154. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/telemetry.py +0 -0
  155. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/__init__.py +0 -0
  156. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/archive_extraction.py +0 -0
  157. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/content_extraction.py +0 -0
  158. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/dependency_groups.py +0 -0
  159. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/external_video.py +0 -0
  160. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/file_metadata.py +0 -0
  161. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/file_to_images.py +0 -0
  162. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/hashing.py +0 -0
  163. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/legacy_office.py +0 -0
  164. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/resources.py +0 -0
  165. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/transcription.py +0 -0
  166. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/uv_sync.py +0 -0
  167. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/validation.py +0 -0
  168. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/video_processing.py +0 -0
  169. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/__init__.py +0 -0
  170. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/_lakehouse_fakes.py +0 -0
  171. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/conftest.py +0 -0
  172. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/__init__.py +0 -0
  173. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  174. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/conftest.py +0 -0
  175. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/content/__init__.py +0 -0
  176. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/__init__.py +0 -0
  177. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/conftest.py +0 -0
  178. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  179. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  180. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_llm_runner.py +0 -0
  181. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  182. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_regex_runner.py +0 -0
  183. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_runner_hardening.py +0 -0
  184. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_transformer_runners.py +0 -0
  185. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/__init__.py +0 -0
  186. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/conftest.py +0 -0
  187. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/sample_invoice.pdf +0 -0
  188. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_chunking.py +0 -0
  189. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_detector.py +0 -0
  190. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  191. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  192. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_severity.py +0 -0
  193. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/secrets/__init__.py +0 -0
  194. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  195. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  196. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_base_detector.py +0 -0
  197. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  198. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  199. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_detector_pipeline_types.py +0 -0
  200. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_detector_schema_examples.py +0 -0
  201. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_detector_types.py +0 -0
  202. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_phase2_detectors.py +0 -0
  203. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_registry.py +0 -0
  204. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/threat/__init__.py +0 -0
  205. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/threat/test_code_security_detector.py +0 -0
  206. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/threat/test_yara_detector.py +0 -0
  207. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  208. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/integration/test_wordpress_links_assets.py +0 -0
  209. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/pipeline/test_detector_outcomes.py +0 -0
  210. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/pipeline/test_detector_pipeline.py +0 -0
  211. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/pipeline/test_worker_pool.py +0 -0
  212. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_assets_metadata_catalog.py +0 -0
  213. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_azure_blob_storage_source.py +0 -0
  214. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_base_source_attachment.py +0 -0
  215. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_base_source_sampling.py +0 -0
  216. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_config.py +0 -0
  217. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_confluence_source.py +0 -0
  218. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_custom_extractor.py +0 -0
  219. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_databricks_source.py +0 -0
  220. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_delta_lake_source.py +0 -0
  221. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_dependency_groups.py +0 -0
  222. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_dropbox_auth_command.py +0 -0
  223. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_dropbox_source.py +0 -0
  224. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_elasticsearch_source.py +0 -0
  225. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_email_source.py +0 -0
  226. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_file_evaluation_runner.py +0 -0
  227. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_google_cloud_storage_source.py +0 -0
  228. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_google_workspace_source.py +0 -0
  229. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_hashing.py +0 -0
  230. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_hive_source.py +0 -0
  231. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_hugging_face_source.py +0 -0
  232. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_iceberg_source.py +0 -0
  233. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_jira_source.py +0 -0
  234. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_kafka_source.py +0 -0
  235. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_local_folder_source.py +0 -0
  236. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_main.py +0 -0
  237. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_meilisearch_source.py +0 -0
  238. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_microsoft_365_source.py +0 -0
  239. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_mongodb_source.py +0 -0
  240. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_mssql_source.py +0 -0
  241. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_mysql_source.py +0 -0
  242. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_neo4j_source.py +0 -0
  243. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_notion_source.py +0 -0
  244. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_object_storage_download_efficiency.py +0 -0
  245. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_opensearch_source.py +0 -0
  246. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_oracle_source.py +0 -0
  247. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_outputs.py +0 -0
  248. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_payload_window.py +0 -0
  249. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_payload_window_parquet.py +0 -0
  250. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_postgresql_source.py +0 -0
  251. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_powerbi_source.py +0 -0
  252. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_recipe_normalizer.py +0 -0
  253. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_reddit_source.py +0 -0
  254. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_s3_compatible_storage_source.py +0 -0
  255. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_sampling_automatic.py +0 -0
  256. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_sandbox_source.py +0 -0
  257. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_scan_cache.py +0 -0
  258. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_servicedesk_source.py +0 -0
  259. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_slack_source.py +0 -0
  260. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_snowflake_source.py +0 -0
  261. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_source_dependency_groups.py +0 -0
  262. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_source_strategy_conformance.py +0 -0
  263. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_sqlite_source.py +0 -0
  264. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_tableau_source.py +0 -0
  265. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_tabular_automatic_sampling.py +0 -0
  266. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_tabular_utils.py +0 -0
  267. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_uv_sync.py +0 -0
  268. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_wordpress_source.py +0 -0
  269. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_youtube_source.py +0 -0
  270. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_youtube_source_integration.py +0 -0
  271. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_archive_extraction.py +0 -0
  272. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_content_extraction.py +0 -0
  273. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_file_metadata.py +0 -0
  274. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_file_parser.py +0 -0
  275. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_file_to_images.py +0 -0
  276. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_legacy_office.py +0 -0
  277. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_transcription.py +0 -0
  278. {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_video_processing.py +0 -0
@@ -0,0 +1,3 @@
1
+ $ uv sync
2
+ Resolved 307 packages in 325ms
3
+ Checked 51 packages in 3ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.80
3
+ Version: 0.4.82
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.4.80",
3
+ "version": "0.4.82",
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.4.80"
3
+ version = "0.4.82"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -32,6 +32,7 @@ from ...models.generated_single_asset_scan_results import (
32
32
  from ...models.generated_single_asset_scan_results import (
33
33
  SingleAssetScanResults,
34
34
  )
35
+ from ...utils.file_parser import render_bytes_cell
35
36
  from ..dependencies import require_module
36
37
  from ..tabular_base import BaseTabularSource
37
38
  from ..tabular_utils import TableRef
@@ -418,7 +419,8 @@ class DatabricksSource(BaseTabularSource):
418
419
  if value is None:
419
420
  return "null"
420
421
  if isinstance(value, (bytes, bytearray, memoryview)):
421
- return f"<{len(bytes(value))} bytes>"
422
+ # Text stored in a BINARY column must still reach the detectors.
423
+ return render_bytes_cell(bytes(value))
422
424
  if isinstance(value, datetime):
423
425
  return value.isoformat()
424
426
  return str(value)
@@ -40,6 +40,7 @@ from ...models.generated_single_asset_scan_results import (
40
40
  from ...models.generated_single_asset_scan_results import (
41
41
  SingleAssetScanResults,
42
42
  )
43
+ from ...utils.file_parser import render_bytes_cell
43
44
  from ...utils.hashing import hash_id
44
45
  from ..base import BaseSource
45
46
  from ..dependencies import require_module
@@ -519,10 +520,10 @@ class KafkaSource(BaseSource):
519
520
  if value is None:
520
521
  return "null"
521
522
  if isinstance(value, (bytes, bytearray)):
522
- try:
523
- return bytes(value).decode("utf-8")
524
- except UnicodeDecodeError:
525
- return f"<{len(bytes(value))} bytes>"
523
+ # A decode that succeeds is not proof of readability: control bytes
524
+ # are valid UTF-8, so the old UnicodeDecodeError fallback never fired
525
+ # for a binary payload and emitted raw control characters instead.
526
+ return render_bytes_cell(bytes(value))
526
527
  return str(value)
527
528
 
528
529
  def _start_offset(self, strategy: SamplingStrategy, low: int, high: int, per: int) -> int:
@@ -25,6 +25,7 @@ from ...models.generated_single_asset_scan_results import (
25
25
  Location,
26
26
  SingleAssetScanResults,
27
27
  )
28
+ from ...utils.file_parser import json_safe_default
28
29
  from ...utils.hashing import hash_id, unhash_id
29
30
  from ..base import BaseSource
30
31
  from ..dependencies import require_module
@@ -434,7 +435,10 @@ class MongoDBSource(BaseSource):
434
435
  )
435
436
 
436
437
  def _serialize_document(self, document: dict[str, Any]) -> str:
437
- return json.dumps(document, ensure_ascii=False, default=str, sort_keys=True)
438
+ # json_safe_default, not default=str: a BSON Binary subclasses bytes, so
439
+ # str() renders its Python repr and a JSON payload stored in one arrived
440
+ # at the detectors doubly escaped instead of as the document it is.
441
+ return json.dumps(document, ensure_ascii=False, default=json_safe_default, sort_keys=True)
438
442
 
439
443
  def _format_collection_content(
440
444
  self,
@@ -24,6 +24,7 @@ from ...models.generated_single_asset_scan_results import (
24
24
  Location,
25
25
  SingleAssetScanResults,
26
26
  )
27
+ from ...utils.file_parser import json_safe_default
27
28
  from ...utils.hashing import hash_id, unhash_id
28
29
  from ..base import BaseSource
29
30
  from ..dependencies import require_module
@@ -434,7 +435,9 @@ class Neo4jSource(BaseSource):
434
435
  return nodes
435
436
 
436
437
  def _serialize_node(self, props: dict[str, Any]) -> str:
437
- return json.dumps(props, ensure_ascii=False, default=str, sort_keys=True)
438
+ # Byte-array properties must not fall to str(), which renders
439
+ # bytearray(b'...') instead of the text the property holds.
440
+ return json.dumps(props, ensure_ascii=False, default=json_safe_default, sort_keys=True)
438
441
 
439
442
  def _format_label_content(
440
443
  self,
@@ -11,6 +11,7 @@ from ...models.generated_input import (
11
11
  SamplingConfig,
12
12
  SamplingStrategy,
13
13
  )
14
+ from ...utils.file_parser import render_bytes_cell
14
15
  from ..dependencies import require_module
15
16
  from ..tabular_base import BaseTabularSource
16
17
  from ..tabular_utils import TableRef
@@ -244,7 +245,10 @@ class OracleSource(BaseTabularSource):
244
245
  value = str(value)
245
246
 
246
247
  if isinstance(value, (bytes, bytearray)):
247
- return f"<{len(value)} bytes>"
248
+ # A CLOB read through the LOB branch above lands here as bytes; text in
249
+ # a BLOB is just as common. Both must stay readable — see
250
+ # ``render_bytes_cell``.
251
+ return render_bytes_cell(bytes(value))
248
252
  if isinstance(value, datetime):
249
253
  return value.isoformat()
250
254
  return str(value)
@@ -21,6 +21,7 @@ from ...models.generated_input import (
21
21
  SnowflakeRequiredKeyPairAuthenticator,
22
22
  SnowflakeRequiredOauthAuthenticatorToken,
23
23
  )
24
+ from ...utils.file_parser import render_bytes_cell
24
25
  from ..dependencies import require_module
25
26
  from ..tabular_base import BaseTabularSource
26
27
  from ..tabular_utils import TableRef
@@ -299,7 +300,8 @@ class SnowflakeSource(BaseTabularSource):
299
300
  if isinstance(value, memoryview):
300
301
  value = value.tobytes()
301
302
  if isinstance(value, (bytes, bytearray)):
302
- return f"<{len(value)} bytes>"
303
+ # Text stored in a BINARY column must still reach the detectors.
304
+ return render_bytes_cell(bytes(value))
303
305
  if isinstance(value, (datetime, date)):
304
306
  return value.isoformat()
305
307
  if isinstance(value, Decimal):
@@ -28,6 +28,7 @@ from ..models.generated_single_asset_scan_results import (
28
28
  SingleAssetScanResults,
29
29
  )
30
30
  from ..utils.file_metadata import build_columns
31
+ from ..utils.file_parser import render_bytes_cell
31
32
  from ..utils.hashing import hash_id, unhash_id
32
33
  from .base import BaseSource
33
34
  from .tabular_utils import TableRef, build_tabular_location, format_tabular_sample_content
@@ -484,7 +485,11 @@ class BaseTabularSource(BaseSource):
484
485
  if isinstance(value, memoryview):
485
486
  value = value.tobytes()
486
487
  if isinstance(value, (bytes, bytearray)):
487
- return f"<{len(value)} bytes>"
488
+ # BYTEA/BLOB/VARBINARY columns routinely hold text — a JSON payload, an
489
+ # encoded document, a note written by an ORM that picked the wrong type.
490
+ # Summarizing them unconditionally meant no detector ever read a byte of
491
+ # it. Genuinely binary values still summarize.
492
+ return render_bytes_cell(bytes(value))
488
493
  if isinstance(value, datetime):
489
494
  return value.isoformat()
490
495
  return str(value)
@@ -34,6 +34,7 @@ from .file_parser import (
34
34
  _normalize_mime_type,
35
35
  _parquet_row_group_start,
36
36
  _require_file_processing,
37
+ is_readable_text,
37
38
  resolve_mime_type,
38
39
  )
39
40
 
@@ -75,29 +76,46 @@ class EmbeddedFile:
75
76
  mime_type: str # resolved from the bytes (and the cell's own path, when it has one)
76
77
 
77
78
 
79
+ # What a byte-carrying parquet column holds, and therefore how a row renders it.
80
+ CONTENT_FILE = "file" # a whole file: becomes a child asset, row text gets a placeholder
81
+ CONTENT_TEXT = "text" # text carried as bytes: decoded straight into the row text
82
+ CONTENT_OPAQUE = "opaque" # bytes that are not a file and not text: placeholder only
83
+
84
+ # Markup and data serializations read fine as raw text and belong to their row.
85
+ # Deliberately not here: message/rfc822, which is a real document format the file
86
+ # parser splits into headers and body, so it is worth its own asset.
87
+ _TEXTUAL_MIME_TYPES = frozenset({"application/json", "application/xml", "application/xhtml+xml"})
88
+
89
+
78
90
  @dataclass(frozen=True)
79
91
  class EmbeddedColumn:
80
- """A parquet column whose cells hold whole files instead of values."""
92
+ """A parquet column whose cells hold byte payloads instead of plain values."""
81
93
 
82
94
  kind: str # "struct" (a HuggingFace feature: struct<bytes, path>) or "binary"
83
- mime_hint: str # sniffed once from a sample cell; used for row-text placeholders
95
+ mime_hint: str # sniffed once from a sample cell
96
+ content: str # CONTENT_FILE | CONTENT_TEXT | CONTENT_OPAQUE
84
97
 
85
98
 
86
99
  def is_embeddable_file_mime(mime_type: str) -> bool:
87
100
  """Whether sniffed bytes look like a self-contained file worth its own asset.
88
101
 
89
- Text-shaped payloads are excluded deliberately. A binary column holding JSON or
90
- plain text already reads correctly as row text, so promoting it to a child asset
91
- would split one record across two assets and scan the same characters twice.
92
- Unrecognized bytes are excluded too: a column of embedding vectors or digests is
93
- binary without being a file, and each cell must not become an asset.
102
+ Text-shaped payloads are excluded deliberately: a cell holding JSON or plain
103
+ text is a *field of its row*, so it is decoded into the row's own text (see
104
+ ``CONTENT_TEXT``) rather than split off into a second asset. Unrecognized bytes
105
+ are excluded too a column of embedding vectors or digests is binary without
106
+ being a file, and each cell must not become an asset.
94
107
  """
108
+ return classify_embedded_mime(mime_type) == CONTENT_FILE
109
+
110
+
111
+ def classify_embedded_mime(mime_type: str) -> str:
112
+ """Classify sniffed bytes as a file, as text, or as opaque binary."""
95
113
  normalized = _normalize_mime_type(mime_type)
96
114
  if not normalized or normalized == OCTET_STREAM:
97
- return False
98
- if normalized.startswith("text/"):
99
- return False
100
- return normalized not in ("application/json", "application/xml", "application/xhtml+xml")
115
+ return CONTENT_OPAQUE
116
+ if normalized.startswith("text/") or normalized in _TEXTUAL_MIME_TYPES:
117
+ return CONTENT_TEXT
118
+ return CONTENT_FILE
101
119
 
102
120
 
103
121
  def has_embedded_files(mime_type: str) -> bool:
@@ -164,10 +182,20 @@ def embedded_cell_name(cell: object, kind: str) -> str:
164
182
  return ""
165
183
 
166
184
 
167
- def detect_parquet_file_columns(parquet_file: Any) -> dict[str, EmbeddedColumn]:
168
- """Map column name → ``EmbeddedColumn`` for columns whose cells hold files.
185
+ def detect_parquet_payload_columns(parquet_file: Any) -> dict[str, EmbeddedColumn]:
186
+ """Map column name → ``EmbeddedColumn`` for every column whose cells hold bytes.
169
187
 
170
- Returns ``{}`` when pyarrow is unavailable or no such column exists.
188
+ All of them are returned, classified by what the bytes turned out to be, because
189
+ each class needs different handling and none of them may fall through to
190
+ ``str(cell)`` — that renders a Python bytes repr, which is how a column of JSON
191
+ documents reached the detectors as ``b'{"email": ...}'`` with escaped quotes.
192
+
193
+ Classification is per column, sampled once: sniffing every cell of a
194
+ million-row file would cost more than the scan it feeds. A column of uniform
195
+ content — the normal case — is therefore classified correctly, and the
196
+ child-asset path re-checks each cell it emits anyway.
197
+
198
+ Returns ``{}`` when pyarrow is unavailable or no column carries bytes.
171
199
  """
172
200
  try:
173
201
  pa = _require_file_processing("pyarrow")
@@ -201,30 +229,41 @@ def detect_parquet_file_columns(parquet_file: Any) -> dict[str, EmbeddedColumn]:
201
229
 
202
230
  columns: dict[str, EmbeddedColumn] = {}
203
231
  for name, kind in candidates:
204
- mime = _sample_column_mime(sample, name, kind)
232
+ mime, sample_bytes = _sample_column_payload(sample, name, kind)
233
+ content = classify_embedded_mime(mime)
234
+ # The MIME sniffer falls back to text/plain for bytes it cannot place, so a
235
+ # column of packed floats or digests arrives labelled as text. Decoding one
236
+ # would trade a useless repr for a row of invisible control characters.
237
+ if (
238
+ content == CONTENT_TEXT
239
+ and sample_bytes is not None
240
+ and not is_readable_text(sample_bytes)
241
+ ):
242
+ content = CONTENT_OPAQUE
205
243
  # A struct<bytes, …> column is a *declared* file feature (HuggingFace Image,
206
- # Audio, Video), so it stays a file column even when the sample is empty or
207
- # unrecognized. A plain binary column has to prove itself see
208
- # ``is_embeddable_file_mime``.
209
- if kind == "struct" or is_embeddable_file_mime(mime):
210
- columns[name] = EmbeddedColumn(kind=kind, mime_hint=mime)
244
+ # Audio, Video), so an unreadable sample still means "file" rather than the
245
+ # opaque default the alternative is a declared feature silently rendering
246
+ # as a placeholder and never being scanned.
247
+ if content == CONTENT_OPAQUE and kind == "struct" and not mime:
248
+ content = CONTENT_FILE
249
+ columns[name] = EmbeddedColumn(kind=kind, mime_hint=mime, content=content)
211
250
  return columns
212
251
 
213
252
 
214
- def _sample_column_mime(batch: Any, column_name: str, kind: str) -> str:
215
- """MIME of the first non-empty cell in the sample batch, or ""."""
253
+ def _sample_column_payload(batch: Any, column_name: str, kind: str) -> tuple[str, bytes | None]:
254
+ """MIME and bytes of the first non-empty cell in the sample batch."""
216
255
  if batch is None:
217
- return ""
256
+ return "", None
218
257
  try:
219
258
  column = batch.column(column_name)
220
259
  except Exception:
221
- return ""
260
+ return "", None
222
261
  for index in range(len(column)):
223
262
  cell = column[index].as_py()
224
263
  raw = extract_embedded_bytes(cell, kind)
225
264
  if raw:
226
- return resolve_mime_type(raw, file_name=embedded_cell_name(cell, kind))
227
- return ""
265
+ return resolve_mime_type(raw, file_name=embedded_cell_name(cell, kind)), raw
266
+ return "", None
228
267
 
229
268
 
230
269
  def _iter_parquet_files(
@@ -249,7 +288,14 @@ def _iter_parquet_files(
249
288
  logger.warning("Cannot open parquet for embedded-file extraction: %s", exc)
250
289
  return
251
290
 
252
- columns = detect_parquet_file_columns(parquet_file)
291
+ # Only file-bearing columns produce child assets. A text column is decoded into
292
+ # its own row's text by the page iterator, so walking it here would scan the
293
+ # same characters twice under two different asset identities.
294
+ columns = {
295
+ name: column
296
+ for name, column in detect_parquet_payload_columns(parquet_file).items()
297
+ if column.content == CONTENT_FILE
298
+ }
253
299
  if not columns:
254
300
  return
255
301
 
@@ -1064,6 +1064,52 @@ def _decode_bytes(file_bytes: bytes) -> str:
1064
1064
  return decoded.replace("\x00", "")
1065
1065
 
1066
1066
 
1067
+ def is_readable_text(raw: bytes) -> bool:
1068
+ """Whether bytes decode to something a text detector could actually read.
1069
+
1070
+ A successful UTF-8 decode is not the test: ``b"\\x01\\x02\\x03"`` decodes
1071
+ cleanly and reads as nothing. What matters is whether the result is mostly
1072
+ printable, which is what separates a UTF-8 document stored in a BLOB from a
1073
+ packed float array stored in the same column type.
1074
+ """
1075
+ sample = raw[:4096]
1076
+ if not sample:
1077
+ return False
1078
+ if b"\x00" in sample:
1079
+ # UTF-16/32 interleaves a NUL with every character and is still text.
1080
+ return _is_null_byte_unicode_text(sample)
1081
+ decoded = sample.decode("utf-8", errors="replace")
1082
+ printable = sum(1 for char in decoded if char.isprintable() or char.isspace())
1083
+ return printable / len(decoded) >= 0.9
1084
+
1085
+
1086
+ def render_bytes_cell(raw: bytes) -> str:
1087
+ """Render a byte-valued field as detector-visible text.
1088
+
1089
+ Sources hand rows and documents to text detectors as strings, so every
1090
+ byte-valued field has to become one. The two obvious ways are both wrong:
1091
+ ``str(value)`` emits a Python repr — ``b'{"email": ...}'``, with the prefix,
1092
+ escaped quotes and ``\\n`` spelled out — and a flat ``<N bytes>`` summary drops
1093
+ the content entirely, so text stored in a BLOB is never scanned by anything.
1094
+
1095
+ This decodes what is readable and summarizes only what isn't.
1096
+ """
1097
+ if is_readable_text(raw):
1098
+ return _decode_bytes(raw)
1099
+ return f"<{len(raw)} bytes>"
1100
+
1101
+
1102
+ def json_safe_default(value: Any) -> Any:
1103
+ """``json.dumps(default=...)`` that keeps byte fields readable.
1104
+
1105
+ ``default=str`` is the usual choice and it is how a BSON ``Binary`` holding a
1106
+ JSON document reached the detectors as a doubly-escaped repr string.
1107
+ """
1108
+ if isinstance(value, bytes | bytearray | memoryview):
1109
+ return render_bytes_cell(bytes(value))
1110
+ return str(value)
1111
+
1112
+
1067
1113
  def resolve_mime_type(
1068
1114
  file_bytes: bytes,
1069
1115
  *,
@@ -1369,12 +1415,20 @@ def _iter_parquet_pages(
1369
1415
  # (before reading any data) so a bad file can't lock the C++ thread pool.
1370
1416
  pf = pq.ParquetFile(io.BytesIO(file_bytes)) # type: ignore[attr-defined]
1371
1417
 
1372
- # File columns (HF Image/Audio structs, raw file-byte columns) carry whole
1373
- # files that are useless and wasteful as row text they're surfaced
1374
- # separately as child assets. Render a compact placeholder instead.
1375
- from .embedded_files import detect_parquet_file_columns, extract_embedded_bytes
1418
+ # Columns whose cells hold bytes never reach str(): that renders a Python
1419
+ # repr, so a column of JSON documents would arrive at the detectors as
1420
+ # b'{"email": ...}' with escaped quotes. Whole files (HF Image/Audio
1421
+ # structs, file-byte columns) render as a placeholder and are scanned
1422
+ # separately as child assets; text carried as bytes is decoded into the row
1423
+ # it belongs to; anything else is summarized rather than spelled out.
1424
+ from .embedded_files import (
1425
+ CONTENT_FILE,
1426
+ CONTENT_TEXT,
1427
+ detect_parquet_payload_columns,
1428
+ extract_embedded_bytes,
1429
+ )
1376
1430
 
1377
- file_columns = detect_parquet_file_columns(pf)
1431
+ payload_columns = detect_parquet_payload_columns(pf)
1378
1432
 
1379
1433
  begin = max(0, start_row)
1380
1434
  first_group, drop_in_group = _parquet_row_group_start(pf, begin)
@@ -1411,12 +1465,15 @@ def _iter_parquet_pages(
1411
1465
  lines.append(f"row_{abs_row + 1}:")
1412
1466
  for col_i, col in enumerate(col_names):
1413
1467
  cell = batch.column(col_i)[local_idx].as_py()
1414
- if col in file_columns:
1415
- embedded_column = file_columns[col]
1416
- cell_str = _format_embedded_placeholder(
1417
- extract_embedded_bytes(cell, embedded_column.kind),
1418
- embedded_column.mime_hint,
1419
- )
1468
+ if col in payload_columns:
1469
+ payload_column = payload_columns[col]
1470
+ raw = extract_embedded_bytes(cell, payload_column.kind)
1471
+ if payload_column.content == CONTENT_TEXT:
1472
+ cell_str = _decode_bytes(raw) if raw else ""
1473
+ elif payload_column.content == CONTENT_FILE:
1474
+ cell_str = _format_embedded_placeholder(raw, payload_column.mime_hint)
1475
+ else:
1476
+ cell_str = _format_embedded_placeholder(raw, OCTET_STREAM)
1420
1477
  else:
1421
1478
  cell_str = "" if cell is None else str(cell)
1422
1479
  first, *rest = cell_str.splitlines() or [""]
@@ -1470,7 +1527,12 @@ def _format_embedded_placeholder(raw: bytes | None, mime_hint: str = "") -> str:
1470
1527
  The label names what the column holds, since that is the only trace of it left
1471
1528
  in the row text once the child asset carries the content.
1472
1529
  """
1473
- label = "image" if mime_hint.startswith("image/") else (mime_hint or "file")
1530
+ if mime_hint.startswith("image/"):
1531
+ label = "image"
1532
+ elif not mime_hint or mime_hint == OCTET_STREAM:
1533
+ label = "binary"
1534
+ else:
1535
+ label = mime_hint
1474
1536
  if not raw:
1475
1537
  return f"<{label}>"
1476
1538
  size = len(raw)
@@ -0,0 +1,141 @@
1
+ """Byte-valued fields must stay readable, whichever source produced them.
2
+
3
+ Every source hands rows and documents to text detectors as strings, so each one
4
+ has to turn byte-valued fields into text. Two ways of doing that are wrong and
5
+ both were in the tree:
6
+
7
+ * ``str(value)`` emits a Python repr — ``b'{"email": ...}'`` — so quotes arrive
8
+ escaped and newlines arrive spelled out as ``\\n``.
9
+ * a flat ``<N bytes>`` summary drops the content, so text stored in a BLOB, a
10
+ BSON ``Binary`` or a byte-array property was never read by any detector.
11
+
12
+ These are family-wide guards: the rendering lives in one helper, and a regression
13
+ in it silently blinds every source at once.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+
20
+ import pytest
21
+
22
+ from src.utils.file_parser import is_readable_text, json_safe_default, render_bytes_cell
23
+
24
+ TEXT_DOC = b"Contact Grace at grace@example.com about invoice 4471."
25
+ JSON_DOC = b'{"name": "Ada Lovelace", "email": "ada@example.com"}'
26
+ MULTILINE = b"Dear Grace,\n\nInvoice 4471 is overdue.\nContact ada@example.com\n"
27
+ JUNK = b"\x01\x02\x03\x04\x05\x06"
28
+ PNG_HEADER = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
29
+
30
+
31
+ # ── The shared helper ────────────────────────────────────────────────────
32
+
33
+
34
+ def test_is_readable_text_separates_documents_from_blobs() -> None:
35
+ assert is_readable_text(TEXT_DOC)
36
+ assert is_readable_text(JSON_DOC)
37
+ assert is_readable_text(MULTILINE)
38
+ # Control bytes decode as valid UTF-8, so a successful decode proves nothing.
39
+ assert not is_readable_text(JUNK)
40
+ assert not is_readable_text(PNG_HEADER)
41
+ assert not is_readable_text(b"")
42
+
43
+
44
+ def test_is_readable_text_accepts_utf16() -> None:
45
+ """UTF-16 interleaves a NUL with every character and is still text."""
46
+ assert is_readable_text(TEXT_DOC.decode().encode("utf-16"))
47
+
48
+
49
+ def test_render_bytes_cell_decodes_text_and_summarizes_binary() -> None:
50
+ assert render_bytes_cell(TEXT_DOC) == TEXT_DOC.decode()
51
+ assert render_bytes_cell(JSON_DOC) == JSON_DOC.decode()
52
+ assert render_bytes_cell(JUNK) == "<6 bytes>"
53
+ assert render_bytes_cell(b"") == "<0 bytes>"
54
+
55
+
56
+ def test_render_bytes_cell_never_emits_a_python_repr() -> None:
57
+ rendered = render_bytes_cell(MULTILINE)
58
+ assert not rendered.startswith("b'")
59
+ assert "\\n" not in rendered # newlines stayed newlines
60
+ assert rendered.count("\n") == MULTILINE.decode().count("\n")
61
+
62
+
63
+ def test_json_safe_default_keeps_byte_fields_readable() -> None:
64
+ encoded = json.dumps({"payload": JSON_DOC}, default=json_safe_default)
65
+ assert json.loads(encoded)["payload"] == JSON_DOC.decode()
66
+ assert json.loads(json.dumps({"v": JUNK}, default=json_safe_default))["v"] == "<6 bytes>"
67
+
68
+
69
+ # ── Per-source wiring ────────────────────────────────────────────────────
70
+
71
+
72
+ def test_sql_family_serializes_byte_cells_as_text() -> None:
73
+ """Covers postgresql, mysql, mssql, sqlite and hive through the shared base."""
74
+ from src.sources.tabular_base import BaseTabularSource
75
+
76
+ serialize = BaseTabularSource._serialize_cell
77
+ assert serialize(None, JSON_DOC) == JSON_DOC.decode()
78
+ assert serialize(None, bytearray(TEXT_DOC)) == TEXT_DOC.decode()
79
+ # psycopg hands BYTEA back as a memoryview.
80
+ assert serialize(None, memoryview(TEXT_DOC)) == TEXT_DOC.decode()
81
+ assert serialize(None, JUNK) == "<6 bytes>"
82
+ assert serialize(None, None) == "null"
83
+
84
+
85
+ @pytest.mark.parametrize(
86
+ ("module_path", "class_name"),
87
+ [
88
+ ("src.sources.oracle.source", "OracleSource"),
89
+ ("src.sources.databricks.source", "DatabricksSource"),
90
+ ("src.sources.snowflake.source", "SnowflakeSource"),
91
+ ],
92
+ )
93
+ def test_dialect_overrides_serialize_byte_cells_as_text(module_path: str, class_name: str) -> None:
94
+ """These three override _serialize_cell, so the base fix does not reach them."""
95
+ module = pytest.importorskip(module_path)
96
+ source_class = getattr(module, class_name)
97
+
98
+ assert source_class._serialize_cell(None, TEXT_DOC) == TEXT_DOC.decode()
99
+ assert source_class._serialize_cell(None, JUNK) == "<6 bytes>"
100
+
101
+
102
+ def test_mongodb_binary_field_is_not_a_repr() -> None:
103
+ """A BSON Binary subclasses bytes, so default=str rendered its repr.
104
+
105
+ Asserted on plain bytes so the guard runs without pymongo installed — that is
106
+ the same branch ``json_safe_default`` takes for a ``Binary`` — and again on a
107
+ real ``Binary`` wherever the optional driver is available.
108
+ """
109
+ from src.sources.mongodb.source import MongoDBSource
110
+
111
+ payloads = [JSON_DOC]
112
+ try:
113
+ from bson import Binary
114
+
115
+ payloads.append(Binary(JSON_DOC))
116
+ except ImportError:
117
+ pass
118
+
119
+ for payload in payloads:
120
+ document = json.loads(MongoDBSource._serialize_document(None, {"payload": payload}))
121
+ assert document["payload"] == JSON_DOC.decode()
122
+ assert "b'" not in document["payload"]
123
+
124
+
125
+ def test_neo4j_byte_array_property_is_not_a_repr() -> None:
126
+ from src.sources.neo4j.source import Neo4jSource
127
+
128
+ node = json.loads(Neo4jSource._serialize_node(None, {"blob": bytearray(TEXT_DOC)}))
129
+
130
+ assert node["blob"] == TEXT_DOC.decode()
131
+ assert "bytearray(" not in node["blob"]
132
+
133
+
134
+ def test_kafka_summarizes_binary_payloads_instead_of_control_characters() -> None:
135
+ """Regression: the old UnicodeDecodeError fallback never fired for control
136
+ bytes, which are valid UTF-8, so binary messages emitted raw control chars."""
137
+ from src.sources.kafka.source import KafkaSource
138
+
139
+ assert KafkaSource._decode(TEXT_DOC) == TEXT_DOC.decode()
140
+ assert KafkaSource._decode(JUNK) == "<6 bytes>"
141
+ assert KafkaSource._decode(None) == "null"