classifyre-cli 0.4.90__tar.gz → 0.4.91__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 (282) hide show
  1. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/.turbo/turbo-build.log +1 -1
  2. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/package.json +1 -1
  4. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/file_evaluation/runner.py +1 -1
  6. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/atlassian_common.py +6 -1
  7. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/base.py +6 -5
  8. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/confluence/source.py +4 -0
  9. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/email/source.py +6 -1
  10. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/google_workspace/source.py +10 -1
  11. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/hugging_face/source.py +3 -0
  12. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/microsoft_365/source.py +10 -1
  13. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/notion/source.py +4 -0
  14. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/object_storage/base.py +13 -2
  15. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/embedded_files.py +175 -52
  16. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/file_metadata.py +25 -0
  17. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/file_parser.py +293 -38
  18. classifyre_cli-0.4.91/tests/test_arrow_ipc_payloads.py +473 -0
  19. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/uv.lock +5 -5
  20. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/.gitignore +0 -0
  21. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/.python-version +0 -0
  22. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/README.md +0 -0
  23. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/main.py +0 -0
  24. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/scripts/generate_models.py +0 -0
  25. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/__init__.py +0 -0
  26. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/config.py +0 -0
  27. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/__init__.py +0 -0
  28. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/base.py +0 -0
  29. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/broken_links/__init__.py +0 -0
  30. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/broken_links/detector.py +0 -0
  31. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/config.py +0 -0
  32. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/content/__init__.py +0 -0
  33. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/__init__.py +0 -0
  34. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/detector.py +0 -0
  35. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/extractor.py +0 -0
  36. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/runners/__init__.py +0 -0
  37. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/runners/_base.py +0 -0
  38. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/runners/_factory.py +0 -0
  39. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/runners/_gliner2.py +0 -0
  40. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/runners/_image_classification.py +0 -0
  41. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/runners/_llm.py +0 -0
  42. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/runners/_object_detection.py +0 -0
  43. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/runners/_regex.py +0 -0
  44. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/runners/_text_classification.py +0 -0
  45. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/custom/trainer.py +0 -0
  46. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/dependencies.py +0 -0
  47. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/engine_version.py +0 -0
  48. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/pii/__init__.py +0 -0
  49. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/pii/detector.py +0 -0
  50. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/secrets/__init__.py +0 -0
  51. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/secrets/detector.py +0 -0
  52. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/threat/__init__.py +0 -0
  53. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/threat/code_security_detector.py +0 -0
  54. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/detectors/threat/yara_detector.py +0 -0
  55. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/file_evaluation/__init__.py +0 -0
  56. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/main.py +0 -0
  57. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/models/generated_detectors.py +0 -0
  58. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/models/generated_input.py +0 -0
  59. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/models/generated_single_asset_scan_results.py +0 -0
  60. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/outputs/__init__.py +0 -0
  61. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/outputs/base.py +0 -0
  62. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/outputs/console.py +0 -0
  63. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/outputs/factory.py +0 -0
  64. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/outputs/file.py +0 -0
  65. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/outputs/rest.py +0 -0
  66. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/pipeline/__init__.py +0 -0
  67. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/pipeline/content_provider.py +0 -0
  68. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/pipeline/detector_pipeline.py +0 -0
  69. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/pipeline/parsed_content_provider.py +0 -0
  70. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/pipeline/payload_window.py +0 -0
  71. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/pipeline/scan_cache.py +0 -0
  72. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/pipeline/text_artifact.py +0 -0
  73. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/pipeline/worker_pool.py +0 -0
  74. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/__init__.py +0 -0
  75. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/asset_metadata.py +0 -0
  76. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/azure_blob_storage/__init__.py +0 -0
  77. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/azure_blob_storage/source.py +0 -0
  78. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/confluence/__init__.py +0 -0
  79. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/databricks/__init__.py +0 -0
  80. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/databricks/source.py +0 -0
  81. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/delta_lake/__init__.py +0 -0
  82. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/delta_lake/source.py +0 -0
  83. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/dependencies.py +0 -0
  84. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/dropbox/__init__.py +0 -0
  85. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/dropbox/auth.py +0 -0
  86. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/dropbox/source.py +0 -0
  87. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/elasticsearch/__init__.py +0 -0
  88. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/elasticsearch/source.py +0 -0
  89. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/email/__init__.py +0 -0
  90. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/google_cloud_storage/__init__.py +0 -0
  91. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/google_cloud_storage/source.py +0 -0
  92. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/google_workspace/__init__.py +0 -0
  93. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/hive/__init__.py +0 -0
  94. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/hive/source.py +0 -0
  95. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/hugging_face/__init__.py +0 -0
  96. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/iceberg/__init__.py +0 -0
  97. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/iceberg/source.py +0 -0
  98. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/jira/__init__.py +0 -0
  99. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/jira/source.py +0 -0
  100. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/kafka/__init__.py +0 -0
  101. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/kafka/rest.py +0 -0
  102. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/kafka/source.py +0 -0
  103. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/lakehouse_base.py +0 -0
  104. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/local_folder/__init__.py +0 -0
  105. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/local_folder/source.py +0 -0
  106. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/meilisearch/__init__.py +0 -0
  107. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/meilisearch/source.py +0 -0
  108. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/microsoft_365/__init__.py +0 -0
  109. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/mongodb/__init__.py +0 -0
  110. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/mongodb/source.py +0 -0
  111. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/mssql/__init__.py +0 -0
  112. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/mssql/source.py +0 -0
  113. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/mysql/__init__.py +0 -0
  114. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/mysql/source.py +0 -0
  115. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/neo4j/__init__.py +0 -0
  116. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/neo4j/source.py +0 -0
  117. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/notion/__init__.py +0 -0
  118. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/notion/client.py +0 -0
  119. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/opensearch/__init__.py +0 -0
  120. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/opensearch/source.py +0 -0
  121. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/oracle/__init__.py +0 -0
  122. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/oracle/source.py +0 -0
  123. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/postgresql/__init__.py +0 -0
  124. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/postgresql/source.py +0 -0
  125. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/powerbi/__init__.py +0 -0
  126. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/powerbi/source.py +0 -0
  127. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/recipe_normalizer.py +0 -0
  128. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/reddit/__init__.py +0 -0
  129. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/reddit/source.py +0 -0
  130. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/s3_client.py +0 -0
  131. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/s3_compatible_storage/README.md +0 -0
  132. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/s3_compatible_storage/__init__.py +0 -0
  133. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/s3_compatible_storage/source.py +0 -0
  134. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/sandbox/__init__.py +0 -0
  135. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/sandbox/source.py +0 -0
  136. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/search_engine_base.py +0 -0
  137. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/servicedesk/__init__.py +0 -0
  138. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/servicedesk/source.py +0 -0
  139. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/slack/__init__.py +0 -0
  140. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/slack/source.py +0 -0
  141. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/snowflake/__init__.py +0 -0
  142. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/snowflake/source.py +0 -0
  143. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/sqlite/__init__.py +0 -0
  144. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/sqlite/source.py +0 -0
  145. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/tableau/__init__.py +0 -0
  146. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/tableau/source.py +0 -0
  147. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/tabular_base.py +0 -0
  148. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/tabular_utils.py +0 -0
  149. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/wordpress/__init__.py +0 -0
  150. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/wordpress/source.py +0 -0
  151. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/youtube/__init__.py +0 -0
  152. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/sources/youtube/source.py +0 -0
  153. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/telemetry.py +0 -0
  154. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/__init__.py +0 -0
  155. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/archive_extraction.py +0 -0
  156. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/content_extraction.py +0 -0
  157. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/dependency_groups.py +0 -0
  158. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/external_video.py +0 -0
  159. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/file_to_images.py +0 -0
  160. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/hashing.py +0 -0
  161. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/legacy_office.py +0 -0
  162. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/payload.py +0 -0
  163. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/range_reader.py +0 -0
  164. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/resources.py +0 -0
  165. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/transcription.py +0 -0
  166. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/uv_sync.py +0 -0
  167. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/validation.py +0 -0
  168. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/src/utils/video_processing.py +0 -0
  169. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/__init__.py +0 -0
  170. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/_lakehouse_fakes.py +0 -0
  171. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/conftest.py +0 -0
  172. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/__init__.py +0 -0
  173. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  174. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/conftest.py +0 -0
  175. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/content/__init__.py +0 -0
  176. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/custom/__init__.py +0 -0
  177. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/custom/conftest.py +0 -0
  178. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  179. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  180. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/custom/test_llm_runner.py +0 -0
  181. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  182. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/custom/test_regex_runner.py +0 -0
  183. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/custom/test_runner_hardening.py +0 -0
  184. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/custom/test_transformer_runners.py +0 -0
  185. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/pii/__init__.py +0 -0
  186. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/pii/conftest.py +0 -0
  187. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/pii/sample_invoice.pdf +0 -0
  188. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/pii/test_pii_chunking.py +0 -0
  189. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/pii/test_pii_detector.py +0 -0
  190. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  191. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  192. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/pii/test_pii_severity.py +0 -0
  193. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/secrets/__init__.py +0 -0
  194. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  195. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  196. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/test_base_detector.py +0 -0
  197. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  198. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  199. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/test_detector_pipeline_types.py +0 -0
  200. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/test_detector_schema_examples.py +0 -0
  201. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/test_detector_types.py +0 -0
  202. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/test_phase2_detectors.py +0 -0
  203. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/test_registry.py +0 -0
  204. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/threat/__init__.py +0 -0
  205. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/threat/test_code_security_detector.py +0 -0
  206. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/detectors/threat/test_yara_detector.py +0 -0
  207. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  208. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/integration/test_wordpress_links_assets.py +0 -0
  209. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/pipeline/test_detector_outcomes.py +0 -0
  210. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/pipeline/test_detector_pipeline.py +0 -0
  211. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/pipeline/test_worker_pool.py +0 -0
  212. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_assets_metadata_catalog.py +0 -0
  213. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_azure_blob_storage_source.py +0 -0
  214. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_base_source_attachment.py +0 -0
  215. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_base_source_sampling.py +0 -0
  216. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_byte_valued_fields.py +0 -0
  217. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_config.py +0 -0
  218. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_confluence_source.py +0 -0
  219. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_custom_extractor.py +0 -0
  220. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_databricks_source.py +0 -0
  221. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_delta_lake_source.py +0 -0
  222. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_dependency_groups.py +0 -0
  223. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_dropbox_auth_command.py +0 -0
  224. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_dropbox_source.py +0 -0
  225. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_elasticsearch_source.py +0 -0
  226. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_email_source.py +0 -0
  227. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_file_evaluation_runner.py +0 -0
  228. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_google_cloud_storage_source.py +0 -0
  229. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_google_workspace_source.py +0 -0
  230. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_hashing.py +0 -0
  231. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_hive_source.py +0 -0
  232. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_hugging_face_source.py +0 -0
  233. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_iceberg_source.py +0 -0
  234. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_jira_source.py +0 -0
  235. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_kafka_source.py +0 -0
  236. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_local_folder_source.py +0 -0
  237. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_main.py +0 -0
  238. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_meilisearch_source.py +0 -0
  239. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_microsoft_365_source.py +0 -0
  240. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_mongodb_source.py +0 -0
  241. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_mssql_source.py +0 -0
  242. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_mysql_source.py +0 -0
  243. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_neo4j_source.py +0 -0
  244. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_notion_source.py +0 -0
  245. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_object_storage_download_efficiency.py +0 -0
  246. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_opensearch_source.py +0 -0
  247. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_oracle_source.py +0 -0
  248. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_outputs.py +0 -0
  249. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_payload_window.py +0 -0
  250. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_payload_window_parquet.py +0 -0
  251. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_postgresql_source.py +0 -0
  252. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_powerbi_source.py +0 -0
  253. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_range_reading.py +0 -0
  254. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_recipe_normalizer.py +0 -0
  255. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_reddit_source.py +0 -0
  256. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_s3_compatible_storage_source.py +0 -0
  257. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_sampling_automatic.py +0 -0
  258. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_sandbox_source.py +0 -0
  259. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_scan_cache.py +0 -0
  260. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_servicedesk_source.py +0 -0
  261. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_slack_source.py +0 -0
  262. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_snowflake_source.py +0 -0
  263. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_source_dependency_groups.py +0 -0
  264. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_source_strategy_conformance.py +0 -0
  265. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_sqlite_source.py +0 -0
  266. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_tableau_source.py +0 -0
  267. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_tabular_automatic_sampling.py +0 -0
  268. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_tabular_utils.py +0 -0
  269. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_uv_sync.py +0 -0
  270. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_wordpress_source.py +0 -0
  271. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_youtube_source.py +0 -0
  272. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/test_youtube_source_integration.py +0 -0
  273. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_archive_extraction.py +0 -0
  274. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_content_extraction.py +0 -0
  275. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_embedded_files.py +0 -0
  276. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_file_metadata.py +0 -0
  277. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_file_parser.py +0 -0
  278. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_file_to_images.py +0 -0
  279. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_legacy_office.py +0 -0
  280. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_payload.py +0 -0
  281. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_transcription.py +0 -0
  282. {classifyre_cli-0.4.90 → classifyre_cli-0.4.91}/tests/utils/test_video_processing.py +0 -0
@@ -1,3 +1,3 @@
1
1
  $ uv sync
2
- Resolved 307 packages in 235ms
2
+ Resolved 307 packages in 249ms
3
3
  Checked 51 packages in 1ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.90
3
+ Version: 0.4.91
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.90",
3
+ "version": "0.4.91",
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.90"
3
+ version = "0.4.91"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -148,7 +148,7 @@ class FileEvaluationRunner:
148
148
  file_mime,
149
149
  )
150
150
 
151
- # Files that embed whole other files (parquet file columns, office media)
151
+ # Files that embed whole other files (parquet/Arrow file columns, office media)
152
152
  # get each embedded file run through the binary detectors directly, with
153
153
  # findings tagged by the embedded location so the UI can group them.
154
154
  embedded_files = has_embedded_files(mime_type)
@@ -11,7 +11,7 @@ from urllib.parse import urljoin, urlsplit, urlunsplit
11
11
 
12
12
  import requests
13
13
 
14
- from ..utils.file_parser import normalize_mime_type
14
+ from ..utils.file_parser import ARROW_MIME_TYPES, normalize_mime_type
15
15
  from ..utils.hashing import normalize_http_url
16
16
 
17
17
  logger = logging.getLogger(__name__)
@@ -25,6 +25,7 @@ TABULAR_MIME_TYPES = {
25
25
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
26
26
  "application/parquet",
27
27
  "application/vnd.apache.parquet",
28
+ *ARROW_MIME_TYPES,
28
29
  }
29
30
 
30
31
  TABULAR_FILE_EXTENSIONS = {
@@ -33,6 +34,10 @@ TABULAR_FILE_EXTENSIONS = {
33
34
  ".xls",
34
35
  ".xlsx",
35
36
  ".parquet",
37
+ ".arrow",
38
+ ".arrows",
39
+ ".feather",
40
+ ".ipc",
36
41
  }
37
42
 
38
43
 
@@ -399,7 +399,7 @@ class BaseSource(ABC):
399
399
  unchanged.
400
400
 
401
401
  ``file_bytes`` may be a seekable binary handle instead of bytes (Parquet
402
- only). The window then bounds what is *transferred*, not just what is
402
+ and Arrow). The window then bounds what is *transferred*, not just what is
403
403
  decoded: a source that can serve byte ranges reads the footer and the one
404
404
  row group this run needs out of an object it never downloads whole.
405
405
  """
@@ -432,11 +432,11 @@ class BaseSource(ABC):
432
432
  def _payload_rows_per_page_unit(mime_type: str, batch_size: int) -> int:
433
433
  """How many payload rows one page from ``iter_file_pages`` carries.
434
434
 
435
- Record-shaped readers (Parquet, CSV/TSV) emit a page per row. Spreadsheets
436
- reach the detectors as extracted text, which is paged in blocks of
437
- ``batch_size`` lines — one line per sheet row.
435
+ Record-shaped readers (Parquet, Arrow IPC, CSV/TSV) emit a page per row.
436
+ Spreadsheets reach the detectors as extracted text, which is paged in
437
+ blocks of ``batch_size`` lines — one line per sheet row.
438
438
  """
439
- from ..utils.file_parser import normalize_mime_type
439
+ from ..utils.file_parser import ARROW_MIME_TYPES, normalize_mime_type
440
440
 
441
441
  normalized = normalize_mime_type(mime_type)
442
442
  record_shaped = {
@@ -444,6 +444,7 @@ class BaseSource(ABC):
444
444
  "application/vnd.apache.parquet",
445
445
  "text/csv",
446
446
  "text/tab-separated-values",
447
+ *ARROW_MIME_TYPES,
447
448
  }
448
449
  return 1 if normalized in record_shaped else max(1, batch_size)
449
450
 
@@ -71,6 +71,10 @@ FILE_EXTENSION_HINTS: dict[str, OutputAssetType] = {
71
71
  ".tar": OutputAssetType.BINARY,
72
72
  ".gz": OutputAssetType.BINARY,
73
73
  ".parquet": OutputAssetType.TABLE,
74
+ ".arrow": OutputAssetType.TABLE,
75
+ ".arrows": OutputAssetType.TABLE,
76
+ ".feather": OutputAssetType.TABLE,
77
+ ".ipc": OutputAssetType.TABLE,
74
78
  ".json": OutputAssetType.TXT,
75
79
  ".xml": OutputAssetType.TXT,
76
80
  ".txt": OutputAssetType.TXT,
@@ -25,7 +25,11 @@ from ...models.generated_single_asset_scan_results import (
25
25
  )
26
26
  from ...utils.content_extraction import html_to_text
27
27
  from ...utils.file_metadata import extract_file_metadata
28
- from ...utils.file_parser import normalize_mime_type, resolve_mime_type
28
+ from ...utils.file_parser import (
29
+ ARROW_MIME_TYPES,
30
+ normalize_mime_type,
31
+ resolve_mime_type,
32
+ )
29
33
  from ...utils.hashing import hash_id
30
34
  from ..base import BaseSource
31
35
  from ..dependencies import require_module
@@ -39,6 +43,7 @@ _TABULAR_MIME_TYPES = {
39
43
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
40
44
  "application/parquet",
41
45
  "application/vnd.apache.parquet",
46
+ *ARROW_MIME_TYPES,
42
47
  }
43
48
 
44
49
  # MIME types whose text can be extracted (by file_parser) and so should route
@@ -28,7 +28,11 @@ from ...models.generated_single_asset_scan_results import (
28
28
  SingleAssetScanResults,
29
29
  )
30
30
  from ...utils.file_metadata import extract_file_metadata
31
- from ...utils.file_parser import normalize_mime_type, resolve_mime_type
31
+ from ...utils.file_parser import (
32
+ ARROW_MIME_TYPES,
33
+ normalize_mime_type,
34
+ resolve_mime_type,
35
+ )
32
36
  from ...utils.hashing import hash_id
33
37
  from ..base import BaseSource
34
38
  from ..dependencies import require_module
@@ -85,6 +89,7 @@ _TABULAR_MIME_TYPES = {
85
89
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
86
90
  "application/parquet",
87
91
  "application/vnd.apache.parquet",
92
+ *ARROW_MIME_TYPES,
88
93
  }
89
94
 
90
95
  _FILE_EXTENSION_HINTS: dict[str, OutputAssetType] = {
@@ -109,6 +114,10 @@ _FILE_EXTENSION_HINTS: dict[str, OutputAssetType] = {
109
114
  ".xlsx": OutputAssetType.TABLE,
110
115
  ".pptx": OutputAssetType.BINARY,
111
116
  ".parquet": OutputAssetType.TABLE,
117
+ ".arrow": OutputAssetType.TABLE,
118
+ ".arrows": OutputAssetType.TABLE,
119
+ ".feather": OutputAssetType.TABLE,
120
+ ".ipc": OutputAssetType.TABLE,
112
121
  ".json": OutputAssetType.TXT,
113
122
  ".xml": OutputAssetType.TXT,
114
123
  ".txt": OutputAssetType.TXT,
@@ -450,6 +450,9 @@ class HuggingFaceSource(ObjectStorageSourceBase):
450
450
  where the row groups are — so it yields not fewer rows but none. Reading
451
451
  by range inverts the cost: the sampling window picks the rows, and only
452
452
  the footer plus the row groups holding those rows ever cross the wire.
453
+ Arrow IPC shards are read the same way — their footer indexes batches by
454
+ offset rather than by row, so a window costs the batches up to its end
455
+ instead of the whole shard.
453
456
 
454
457
  The Hub's ``resolve`` endpoint and its CDN both honour ``Range``. If a
455
458
  deployment ever does not, ``HttpRangeReader`` still returns correct bytes
@@ -32,7 +32,11 @@ from ...models.generated_single_asset_scan_results import (
32
32
  SingleAssetScanResults,
33
33
  )
34
34
  from ...utils.file_metadata import extract_file_metadata
35
- from ...utils.file_parser import normalize_mime_type, resolve_mime_type
35
+ from ...utils.file_parser import (
36
+ ARROW_MIME_TYPES,
37
+ normalize_mime_type,
38
+ resolve_mime_type,
39
+ )
36
40
  from ...utils.hashing import hash_id
37
41
  from ..base import BaseSource
38
42
  from ..dependencies import require_module
@@ -53,6 +57,7 @@ _TABULAR_MIME_TYPES = {
53
57
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
54
58
  "application/parquet",
55
59
  "application/vnd.apache.parquet",
60
+ *ARROW_MIME_TYPES,
56
61
  }
57
62
 
58
63
  _FILE_EXTENSION_HINTS: dict[str, OutputAssetType] = {
@@ -77,6 +82,10 @@ _FILE_EXTENSION_HINTS: dict[str, OutputAssetType] = {
77
82
  ".xlsx": OutputAssetType.TABLE,
78
83
  ".pptx": OutputAssetType.BINARY,
79
84
  ".parquet": OutputAssetType.TABLE,
85
+ ".arrow": OutputAssetType.TABLE,
86
+ ".arrows": OutputAssetType.TABLE,
87
+ ".feather": OutputAssetType.TABLE,
88
+ ".ipc": OutputAssetType.TABLE,
80
89
  ".json": OutputAssetType.TXT,
81
90
  ".xml": OutputAssetType.TXT,
82
91
  ".txt": OutputAssetType.TXT,
@@ -68,6 +68,10 @@ FILE_EXTENSION_HINTS: dict[str, OutputAssetType] = {
68
68
  ".xls": OutputAssetType.TABLE,
69
69
  ".xlsx": OutputAssetType.TABLE,
70
70
  ".parquet": OutputAssetType.TABLE,
71
+ ".arrow": OutputAssetType.TABLE,
72
+ ".arrows": OutputAssetType.TABLE,
73
+ ".feather": OutputAssetType.TABLE,
74
+ ".ipc": OutputAssetType.TABLE,
71
75
  ".csv": OutputAssetType.TABLE,
72
76
  ".tsv": OutputAssetType.TABLE,
73
77
  ".json": OutputAssetType.TXT,
@@ -38,6 +38,8 @@ from ...utils.embedded_files import (
38
38
  )
39
39
  from ...utils.file_metadata import extract_file_metadata
40
40
  from ...utils.file_parser import (
41
+ ARROW_FILE_MIME_TYPE,
42
+ ARROW_MIME_TYPES,
41
43
  count_tabular_rows,
42
44
  infer_mime_type_from_file_name,
43
45
  normalize_mime_type,
@@ -66,6 +68,7 @@ _TABULAR_MIME_TYPES = {
66
68
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
67
69
  "application/parquet",
68
70
  "application/vnd.apache.parquet",
71
+ *ARROW_MIME_TYPES,
69
72
  }
70
73
 
71
74
 
@@ -109,6 +112,10 @@ _FILE_EXTENSION_HINTS: dict[str, OutputAssetType] = {
109
112
  ".gz": OutputAssetType.BINARY,
110
113
  ".tgz": OutputAssetType.BINARY,
111
114
  ".parquet": OutputAssetType.TABLE,
115
+ ".arrow": OutputAssetType.TABLE,
116
+ ".arrows": OutputAssetType.TABLE,
117
+ ".feather": OutputAssetType.TABLE,
118
+ ".ipc": OutputAssetType.TABLE,
112
119
  ".json": OutputAssetType.TXT,
113
120
  ".xml": OutputAssetType.TXT,
114
121
  ".txt": OutputAssetType.TXT,
@@ -149,8 +156,12 @@ _MAX_PENDING_CONTAINER_BYTES = 256 * 1024 * 1024
149
156
 
150
157
  # Formats a seekable handle buys anything for: the reader can seek to an index
151
158
  # and pull only the parts it needs. Everything else is extracted whole, so a
152
- # handle would just be a slower way to download the object.
153
- _RANGE_READABLE_MIME_TYPES = frozenset({"application/parquet", "application/vnd.apache.parquet"})
159
+ # handle would just be a slower way to download the object. The Arrow *streaming*
160
+ # layout is deliberately absent: it has no index, so reading it through ranges
161
+ # transfers the same bytes in more requests.
162
+ _RANGE_READABLE_MIME_TYPES = frozenset(
163
+ {"application/parquet", "application/vnd.apache.parquet", ARROW_FILE_MIME_TYPE}
164
+ )
154
165
 
155
166
 
156
167
  @dataclass
@@ -1,20 +1,21 @@
1
- """Extract whole files embedded inside parquet rows and OOXML media folders.
1
+ """Extract whole files embedded inside table rows and OOXML media folders.
2
2
 
3
3
  Some files carry other *files* inside them rather than beside them: a HuggingFace
4
4
  dataset stores an ``image`` or ``audio`` column as ``struct<bytes, path>``, a
5
- parquet export can hold a column of PDF or archive blobs, and Office documents
6
- embed media under their ``media/`` folders. This module surfaces those as raw
7
- bytes so the scan pipeline can turn each one into its own child asset — parsed by
8
- ``utils.file_parser`` and scanned like any standalone file — instead of dumping
9
- undecodable bytes into text detectors.
10
-
11
- Parquet extraction is row-addressable. ``start_row``/``max_rows`` bound it to the
12
- same window the sampling strategy applies to that payload's rows (see
13
- ``pipeline.payload_window``), so a run materializes the children of the rows it
14
- actually scanned rather than the first N rows of the file on every run. Row
15
- numbers stay absolute, which is what keeps a child's identity stable: the image in
16
- row 4,000,001 is ``row=4000001;col=image`` whichever window found it. OOXML media
17
- has no row axis, so the bounds are ignored there.
5
+ parquet or Arrow export can hold a column of PDF or archive blobs, and Office
6
+ documents embed media under their ``media/`` folders. This module surfaces those
7
+ as raw bytes so the scan pipeline can turn each one into its own child asset —
8
+ parsed by ``utils.file_parser`` and scanned like any standalone file — instead of
9
+ dumping undecodable bytes into text detectors.
10
+
11
+ Row-shaped containers (parquet, Arrow IPC / Feather) are extracted by row.
12
+ ``start_row``/``max_rows`` bound the walk to the same window the sampling strategy
13
+ applies to that payload's rows (see ``pipeline.payload_window``), so a run
14
+ materializes the children of the rows it actually scanned rather than the first N
15
+ rows of the file on every run. Row numbers stay absolute, which is what keeps a
16
+ child's identity stable: the image in row 4,000,001 is ``row=4000001;col=image``
17
+ whichever window found it, and whichever format it was stored in. OOXML media has
18
+ no row axis, so the bounds are ignored there.
18
19
 
19
20
  On any missing optional dependency or parse failure the iterators log a warning and
20
21
  yield nothing, so callers degrade gracefully.
@@ -30,11 +31,14 @@ from dataclasses import dataclass
30
31
  from typing import Any
31
32
 
32
33
  from .file_parser import (
34
+ ARROW_MIME_TYPES,
33
35
  OCTET_STREAM,
36
+ _arrow_truncation_error,
34
37
  _normalize_mime_type,
35
38
  _parquet_row_group_span,
36
39
  _require_file_processing,
37
40
  is_readable_text,
41
+ open_arrow_batches,
38
42
  resolve_mime_type,
39
43
  )
40
44
 
@@ -121,7 +125,11 @@ def classify_embedded_mime(mime_type: str) -> str:
121
125
  def has_embedded_files(mime_type: str) -> bool:
122
126
  """Return True if this MIME type is a container we can pull whole files out of."""
123
127
  normalized = _normalize_mime_type(mime_type)
124
- return normalized in _PARQUET_MIME_TYPES or normalized in _OOXML_MIME_TYPES
128
+ return (
129
+ normalized in _PARQUET_MIME_TYPES
130
+ or normalized in ARROW_MIME_TYPES
131
+ or normalized in _OOXML_MIME_TYPES
132
+ )
125
133
 
126
134
 
127
135
  def iter_embedded_files(
@@ -132,14 +140,14 @@ def iter_embedded_files(
132
140
  max_rows: int | None = None,
133
141
  max_files: int = DEFAULT_MAX_EMBEDDED_FILES,
134
142
  ) -> Iterator[EmbeddedFile]:
135
- """Yield the files embedded in a parquet or OOXML container.
143
+ """Yield the files embedded in a parquet, Arrow IPC, or OOXML container.
136
144
 
137
- ``start_row``/``max_rows`` bound a parquet walk to one row window and are
145
+ ``start_row``/``max_rows`` bound a row-shaped walk to one row window and are
138
146
  ignored for containers with no row axis.
139
147
 
140
- ``source`` may be a seekable handle rather than bytes (parquet only), which
141
- is how an object too large to download still yields its embedded children:
142
- only the row groups the window covers are transferred. OOXML has no row axis
148
+ ``source`` may be a seekable handle rather than bytes (parquet and Arrow),
149
+ which is how an object too large to download still yields its embedded
150
+ children: only the batches the window covers are read. OOXML has no row axis
143
151
  and is read whole, so it stays on bytes.
144
152
  """
145
153
  is_bytes = isinstance(source, bytes | bytearray)
@@ -153,6 +161,13 @@ def iter_embedded_files(
153
161
  max_rows=max_rows,
154
162
  max_files=max_files,
155
163
  )
164
+ elif normalized in ARROW_MIME_TYPES:
165
+ yield from _iter_arrow_files(
166
+ source,
167
+ start_row=start_row,
168
+ max_rows=max_rows,
169
+ max_files=max_files,
170
+ )
156
171
  elif normalized in _OOXML_MIME_TYPES and is_bytes:
157
172
  yield from _iter_ooxml_files(source, max_files)
158
173
 
@@ -188,33 +203,10 @@ def embedded_cell_name(cell: object, kind: str) -> str:
188
203
  return ""
189
204
 
190
205
 
191
- def detect_parquet_payload_columns(
192
- parquet_file: Any,
193
- *,
194
- sample_row_group: int | None = None,
195
- ) -> dict[str, EmbeddedColumn]:
196
- """Map column name → ``EmbeddedColumn`` for every column whose cells hold bytes.
197
-
198
- All of them are returned, classified by what the bytes turned out to be, because
199
- each class needs different handling and none of them may fall through to
200
- ``str(cell)`` — that renders a Python bytes repr, which is how a column of JSON
201
- documents reached the detectors as ``b'{"email": ...}'`` with escaped quotes.
202
-
203
- Classification is per column, sampled once: sniffing every cell of a
204
- million-row file would cost more than the scan it feeds. A column of uniform
205
- content — the normal case — is therefore classified correctly, and the
206
- child-asset path re-checks each cell it emits anyway.
207
-
208
- Returns ``{}`` when pyarrow is unavailable or no column carries bytes.
209
- """
210
- try:
211
- pa = _require_file_processing("pyarrow")
212
- except Exception as exc: # pragma: no cover - dependency missing
213
- logger.warning("Cannot inspect parquet file columns: %s", exc)
214
- return {}
215
-
206
+ def _byte_carrying_columns(pa: Any, schema: Any) -> list[tuple[str, str]]:
207
+ """``(column name, kind)`` for every field of an Arrow schema holding bytes."""
216
208
  candidates: list[tuple[str, str]] = []
217
- for field in parquet_file.schema_arrow:
209
+ for field in schema:
218
210
  field_type = field.type
219
211
  if pa.types.is_struct(field_type): # type: ignore[attr-defined]
220
212
  child_names = {
@@ -227,15 +219,31 @@ def detect_parquet_payload_columns(
227
219
  field_type
228
220
  ):
229
221
  candidates.append((field.name, "binary"))
222
+ return candidates
230
223
 
231
- if not candidates:
224
+
225
+ def detect_parquet_payload_columns(
226
+ parquet_file: Any,
227
+ *,
228
+ sample_row_group: int | None = None,
229
+ ) -> dict[str, EmbeddedColumn]:
230
+ """``detect_payload_columns`` for a Parquet file, sampled from one row group.
231
+
232
+ Naming the row group matters: handed an open-ended batch iterator, pyarrow
233
+ buffers every group in the file before yielding the first one — which over a
234
+ range-reading handle means downloading the whole object to sniff one batch.
235
+ Callers that already know which group they are about to read pass it, so the
236
+ sample costs nothing beyond the window itself.
237
+ """
238
+ try:
239
+ pa = _require_file_processing("pyarrow")
240
+ except Exception as exc: # pragma: no cover - dependency missing
241
+ logger.warning("Cannot inspect parquet file columns: %s", exc)
242
+ return {}
243
+
244
+ if not _byte_carrying_columns(pa, parquet_file.schema_arrow):
232
245
  return {}
233
246
 
234
- # Naming the row group matters: handed an open-ended batch iterator, pyarrow
235
- # buffers every group in the file before yielding the first one — which over a
236
- # range-reading handle means downloading the whole object to sniff one batch.
237
- # Callers that already know which group they are about to read pass it, so the
238
- # sample costs nothing beyond the window itself.
239
247
  try:
240
248
  sample_kwargs: dict[str, Any] = {"batch_size": _COLUMN_SAMPLE_ROWS}
241
249
  groups = int(parquet_file.metadata.num_row_groups)
@@ -247,6 +255,34 @@ def detect_parquet_payload_columns(
247
255
  logger.warning("Cannot sample parquet columns: %s", exc)
248
256
  return {}
249
257
 
258
+ return detect_payload_columns(parquet_file.schema_arrow, sample)
259
+
260
+
261
+ def detect_payload_columns(schema: Any, sample: Any) -> dict[str, EmbeddedColumn]:
262
+ """Map column name → ``EmbeddedColumn`` for every column whose cells hold bytes.
263
+
264
+ All of them are returned, classified by what the bytes turned out to be, because
265
+ each class needs different handling and none of them may fall through to
266
+ ``str(cell)`` — that renders a Python bytes repr, which is how a column of JSON
267
+ documents reached the detectors as ``b'{"email": ...}'`` with escaped quotes.
268
+
269
+ Classification is per column, sampled once from ``sample`` (one record batch):
270
+ sniffing every cell of a million-row file would cost more than the scan it
271
+ feeds. A column of uniform content — the normal case — is therefore classified
272
+ correctly, and the child-asset path re-checks each cell it emits anyway.
273
+
274
+ Returns ``{}`` when pyarrow is unavailable or no column carries bytes.
275
+ """
276
+ try:
277
+ pa = _require_file_processing("pyarrow")
278
+ except Exception as exc: # pragma: no cover - dependency missing
279
+ logger.warning("Cannot inspect columns for embedded files: %s", exc)
280
+ return {}
281
+
282
+ candidates = _byte_carrying_columns(pa, schema)
283
+ if not candidates:
284
+ return {}
285
+
250
286
  columns: dict[str, EmbeddedColumn] = {}
251
287
  for name, kind in candidates:
252
288
  mime, sample_bytes = _sample_column_payload(sample, name, kind)
@@ -385,6 +421,93 @@ def _iter_parquet_files(
385
421
  logger.warning("Parquet embedded-file iteration failed: %s", exc)
386
422
 
387
423
 
424
+ # ---------------------------------------------------------------------------
425
+ # Arrow IPC / Feather
426
+ # ---------------------------------------------------------------------------
427
+
428
+
429
+ def _iter_arrow_files(
430
+ source: bytes | Any,
431
+ *,
432
+ start_row: int,
433
+ max_rows: int | None,
434
+ max_files: int,
435
+ ) -> Iterator[EmbeddedFile]:
436
+ """The parquet walk over an Arrow payload: same rows, same child identities.
437
+
438
+ A HuggingFace dataset is published as both formats, so an ``image`` column is
439
+ a ``struct<bytes, path>`` either way and has to become the same child asset
440
+ either way — ``row=N;col=image``, numbered absolutely, so a second run updates
441
+ the child rather than duplicating it.
442
+ """
443
+ truncation = _arrow_truncation_error(source)
444
+ if truncation is not None:
445
+ logger.warning("%s Skipping embedded-file extraction.", truncation)
446
+ return
447
+
448
+ try:
449
+ schema, batches = open_arrow_batches(source, batch_rows=_ROW_BATCH_SIZE)
450
+ except Exception as exc:
451
+ logger.warning("Cannot open Arrow payload for embedded-file extraction: %s", exc)
452
+ return
453
+
454
+ begin = max(0, start_row)
455
+ files = 0
456
+ rows_read = 0
457
+ abs_row = 0
458
+ # Only file-bearing columns produce child assets. A text column is decoded into
459
+ # its own row's text by the page iterator, so walking it here would scan the
460
+ # same characters twice under two different asset identities.
461
+ columns: dict[str, EmbeddedColumn] | None = None
462
+ try:
463
+ for batch in batches:
464
+ rows = batch.num_rows
465
+ if abs_row + rows <= begin:
466
+ abs_row += rows
467
+ continue
468
+ if columns is None:
469
+ # Sampled from the first batch of the window, which is being read
470
+ # anyway — the same bargain the parquet walk strikes.
471
+ columns = {
472
+ name: column
473
+ for name, column in detect_payload_columns(schema, batch).items()
474
+ if column.content == CONTENT_FILE
475
+ }
476
+ if not columns:
477
+ return
478
+ for local_index in range(rows):
479
+ if abs_row < begin:
480
+ abs_row += 1
481
+ continue
482
+ if max_rows is not None and rows_read >= max_rows:
483
+ return
484
+ for column_name, column in columns.items():
485
+ cell = batch.column(column_name)[local_index].as_py()
486
+ raw = extract_embedded_bytes(cell, column.kind)
487
+ if not raw:
488
+ continue
489
+ mime = resolve_mime_type(raw, file_name=embedded_cell_name(cell, column.kind))
490
+ if not is_embeddable_file_mime(mime):
491
+ continue
492
+ yield EmbeddedFile(
493
+ location=f"row={abs_row + 1};col={column_name}",
494
+ file_bytes=raw,
495
+ mime_type=mime,
496
+ )
497
+ files += 1
498
+ if files >= max_files:
499
+ logger.info(
500
+ "Reached max embedded files (%d); stopping extraction", max_files
501
+ )
502
+ return
503
+ abs_row += 1
504
+ rows_read += 1
505
+ if max_rows is not None and rows_read >= max_rows:
506
+ return
507
+ except Exception as exc:
508
+ logger.warning("Arrow embedded-file iteration failed: %s", exc)
509
+
510
+
388
511
  # ---------------------------------------------------------------------------
389
512
  # OOXML (xlsx / docx / pptx)
390
513
  # ---------------------------------------------------------------------------
@@ -82,6 +82,8 @@ def extract_file_metadata(
82
82
  metadata.update(_docx_metadata(file_bytes))
83
83
  elif _is_parquet(normalized, extension):
84
84
  metadata.update(_parquet_metadata(file_bytes))
85
+ elif _is_arrow(normalized, extension):
86
+ metadata.update(_arrow_metadata(file_bytes))
85
87
  elif _is_xlsx(normalized, extension):
86
88
  metadata.update(_xlsx_metadata(file_bytes))
87
89
  elif _is_delimited(normalized, extension):
@@ -114,10 +116,17 @@ _IMAGE_EXTENSIONS = {
114
116
  _TEXT_EXTENSIONS = {".txt", ".md", ".xml", ".log", ".yaml", ".yml"}
115
117
 
116
118
 
119
+ _ARROW_EXTENSIONS = {".arrow", ".arrows", ".feather", ".ipc"}
120
+
121
+
117
122
  def _is_parquet(mime: str, extension: str) -> bool:
118
123
  return "parquet" in mime or extension == ".parquet"
119
124
 
120
125
 
126
+ def _is_arrow(mime: str, extension: str) -> bool:
127
+ return "arrow" in mime or "feather" in mime or extension in _ARROW_EXTENSIONS
128
+
129
+
121
130
  def _is_xlsx(mime: str, extension: str) -> bool:
122
131
  return (
123
132
  mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
@@ -189,6 +198,22 @@ def _parquet_metadata(file_bytes: bytes) -> dict[str, Any]:
189
198
  }
190
199
 
191
200
 
201
+ def _arrow_metadata(file_bytes: bytes) -> dict[str, Any]:
202
+ # Arrow's footer indexes record batches by offset and does not total their
203
+ # rows, so unlike Parquet the count has to be walked for. One batch is held at
204
+ # a time and the payload is already resident here, so the walk costs decoding
205
+ # rather than transfer.
206
+ from .file_parser import open_arrow_batches
207
+
208
+ schema, batches = open_arrow_batches(io.BytesIO(file_bytes))
209
+ row_count = sum(batch.num_rows for batch in batches)
210
+ column_types = {field.name: str(field.type) for field in schema}
211
+ return {
212
+ "row_count": row_count,
213
+ "columns": build_columns(list(schema.names), column_types),
214
+ }
215
+
216
+
192
217
  def _xlsx_metadata(file_bytes: bytes) -> dict[str, Any]:
193
218
  import openpyxl # type: ignore[import-untyped]
194
219