classifyre-cli 0.4.82__tar.gz → 0.4.83__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.83/.turbo/turbo-build.log +3 -0
  2. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/package.json +1 -1
  4. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/models/generated_input.py +140 -10
  6. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/pipeline/payload_window.py +58 -12
  7. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/azure_blob_storage/source.py +32 -0
  8. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/base.py +6 -1
  9. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/dropbox/source.py +29 -0
  10. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/google_cloud_storage/source.py +30 -0
  11. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/hugging_face/source.py +64 -0
  12. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/local_folder/source.py +22 -0
  13. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/object_storage/base.py +360 -93
  14. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/s3_compatible_storage/source.py +47 -0
  15. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/embedded_files.py +56 -27
  16. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/file_parser.py +240 -101
  17. classifyre_cli-0.4.83/src/utils/payload.py +165 -0
  18. classifyre_cli-0.4.83/src/utils/range_reader.py +227 -0
  19. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_object_storage_download_efficiency.py +81 -1
  20. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_payload_window.py +120 -0
  21. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_payload_window_parquet.py +71 -4
  22. classifyre_cli-0.4.83/tests/test_range_reading.py +390 -0
  23. classifyre_cli-0.4.83/tests/utils/test_payload.py +123 -0
  24. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/uv.lock +2 -2
  25. classifyre_cli-0.4.82/.turbo/turbo-build.log +0 -3
  26. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/.gitignore +0 -0
  27. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/.python-version +0 -0
  28. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/README.md +0 -0
  29. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/main.py +0 -0
  30. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/scripts/generate_models.py +0 -0
  31. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/__init__.py +0 -0
  32. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/config.py +0 -0
  33. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/__init__.py +0 -0
  34. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/base.py +0 -0
  35. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/broken_links/__init__.py +0 -0
  36. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/broken_links/detector.py +0 -0
  37. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/config.py +0 -0
  38. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/content/__init__.py +0 -0
  39. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/__init__.py +0 -0
  40. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/detector.py +0 -0
  41. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/extractor.py +0 -0
  42. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/runners/__init__.py +0 -0
  43. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/runners/_base.py +0 -0
  44. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/runners/_factory.py +0 -0
  45. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/runners/_gliner2.py +0 -0
  46. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/runners/_image_classification.py +0 -0
  47. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/runners/_llm.py +0 -0
  48. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/runners/_object_detection.py +0 -0
  49. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/runners/_regex.py +0 -0
  50. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/runners/_text_classification.py +0 -0
  51. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/custom/trainer.py +0 -0
  52. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/dependencies.py +0 -0
  53. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/engine_version.py +0 -0
  54. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/pii/__init__.py +0 -0
  55. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/pii/detector.py +0 -0
  56. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/secrets/__init__.py +0 -0
  57. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/secrets/detector.py +0 -0
  58. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/threat/__init__.py +0 -0
  59. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/threat/code_security_detector.py +0 -0
  60. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/detectors/threat/yara_detector.py +0 -0
  61. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/file_evaluation/__init__.py +0 -0
  62. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/file_evaluation/runner.py +0 -0
  63. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/main.py +0 -0
  64. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/models/generated_detectors.py +0 -0
  65. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/models/generated_single_asset_scan_results.py +0 -0
  66. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/outputs/__init__.py +0 -0
  67. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/outputs/base.py +0 -0
  68. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/outputs/console.py +0 -0
  69. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/outputs/factory.py +0 -0
  70. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/outputs/file.py +0 -0
  71. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/outputs/rest.py +0 -0
  72. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/pipeline/__init__.py +0 -0
  73. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/pipeline/content_provider.py +0 -0
  74. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/pipeline/detector_pipeline.py +0 -0
  75. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/pipeline/parsed_content_provider.py +0 -0
  76. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/pipeline/scan_cache.py +0 -0
  77. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/pipeline/text_artifact.py +0 -0
  78. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/pipeline/worker_pool.py +0 -0
  79. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/__init__.py +0 -0
  80. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/asset_metadata.py +0 -0
  81. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/atlassian_common.py +0 -0
  82. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/azure_blob_storage/__init__.py +0 -0
  83. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/confluence/__init__.py +0 -0
  84. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/confluence/source.py +0 -0
  85. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/databricks/__init__.py +0 -0
  86. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/databricks/source.py +0 -0
  87. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/delta_lake/__init__.py +0 -0
  88. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/delta_lake/source.py +0 -0
  89. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/dependencies.py +0 -0
  90. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/dropbox/__init__.py +0 -0
  91. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/dropbox/auth.py +0 -0
  92. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/elasticsearch/__init__.py +0 -0
  93. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/elasticsearch/source.py +0 -0
  94. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/email/__init__.py +0 -0
  95. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/email/source.py +0 -0
  96. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/google_cloud_storage/__init__.py +0 -0
  97. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/google_workspace/__init__.py +0 -0
  98. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/google_workspace/source.py +0 -0
  99. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/hive/__init__.py +0 -0
  100. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/hive/source.py +0 -0
  101. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/hugging_face/__init__.py +0 -0
  102. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/iceberg/__init__.py +0 -0
  103. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/iceberg/source.py +0 -0
  104. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/jira/__init__.py +0 -0
  105. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/jira/source.py +0 -0
  106. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/kafka/__init__.py +0 -0
  107. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/kafka/rest.py +0 -0
  108. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/kafka/source.py +0 -0
  109. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/lakehouse_base.py +0 -0
  110. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/local_folder/__init__.py +0 -0
  111. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/meilisearch/__init__.py +0 -0
  112. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/meilisearch/source.py +0 -0
  113. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/microsoft_365/__init__.py +0 -0
  114. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/microsoft_365/source.py +0 -0
  115. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/mongodb/__init__.py +0 -0
  116. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/mongodb/source.py +0 -0
  117. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/mssql/__init__.py +0 -0
  118. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/mssql/source.py +0 -0
  119. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/mysql/__init__.py +0 -0
  120. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/mysql/source.py +0 -0
  121. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/neo4j/__init__.py +0 -0
  122. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/neo4j/source.py +0 -0
  123. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/notion/__init__.py +0 -0
  124. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/notion/client.py +0 -0
  125. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/notion/source.py +0 -0
  126. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/opensearch/__init__.py +0 -0
  127. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/opensearch/source.py +0 -0
  128. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/oracle/__init__.py +0 -0
  129. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/oracle/source.py +0 -0
  130. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/postgresql/__init__.py +0 -0
  131. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/postgresql/source.py +0 -0
  132. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/powerbi/__init__.py +0 -0
  133. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/powerbi/source.py +0 -0
  134. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/recipe_normalizer.py +0 -0
  135. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/reddit/__init__.py +0 -0
  136. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/reddit/source.py +0 -0
  137. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/s3_client.py +0 -0
  138. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/s3_compatible_storage/README.md +0 -0
  139. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/s3_compatible_storage/__init__.py +0 -0
  140. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/sandbox/__init__.py +0 -0
  141. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/sandbox/source.py +0 -0
  142. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/search_engine_base.py +0 -0
  143. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/servicedesk/__init__.py +0 -0
  144. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/servicedesk/source.py +0 -0
  145. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/slack/__init__.py +0 -0
  146. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/slack/source.py +0 -0
  147. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/snowflake/__init__.py +0 -0
  148. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/snowflake/source.py +0 -0
  149. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/sqlite/__init__.py +0 -0
  150. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/sqlite/source.py +0 -0
  151. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/tableau/__init__.py +0 -0
  152. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/tableau/source.py +0 -0
  153. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/tabular_base.py +0 -0
  154. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/tabular_utils.py +0 -0
  155. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/wordpress/__init__.py +0 -0
  156. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/wordpress/source.py +0 -0
  157. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/youtube/__init__.py +0 -0
  158. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/sources/youtube/source.py +0 -0
  159. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/telemetry.py +0 -0
  160. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/__init__.py +0 -0
  161. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/archive_extraction.py +0 -0
  162. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/content_extraction.py +0 -0
  163. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/dependency_groups.py +0 -0
  164. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/external_video.py +0 -0
  165. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/file_metadata.py +0 -0
  166. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/file_to_images.py +0 -0
  167. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/hashing.py +0 -0
  168. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/legacy_office.py +0 -0
  169. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/resources.py +0 -0
  170. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/transcription.py +0 -0
  171. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/uv_sync.py +0 -0
  172. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/validation.py +0 -0
  173. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/src/utils/video_processing.py +0 -0
  174. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/__init__.py +0 -0
  175. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/_lakehouse_fakes.py +0 -0
  176. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/conftest.py +0 -0
  177. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/__init__.py +0 -0
  178. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  179. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/conftest.py +0 -0
  180. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/content/__init__.py +0 -0
  181. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/custom/__init__.py +0 -0
  182. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/custom/conftest.py +0 -0
  183. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  184. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  185. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/custom/test_llm_runner.py +0 -0
  186. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  187. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/custom/test_regex_runner.py +0 -0
  188. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/custom/test_runner_hardening.py +0 -0
  189. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/custom/test_transformer_runners.py +0 -0
  190. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/pii/__init__.py +0 -0
  191. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/pii/conftest.py +0 -0
  192. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/pii/sample_invoice.pdf +0 -0
  193. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/pii/test_pii_chunking.py +0 -0
  194. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/pii/test_pii_detector.py +0 -0
  195. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  196. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  197. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/pii/test_pii_severity.py +0 -0
  198. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/secrets/__init__.py +0 -0
  199. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  200. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  201. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/test_base_detector.py +0 -0
  202. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  203. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  204. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/test_detector_pipeline_types.py +0 -0
  205. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/test_detector_schema_examples.py +0 -0
  206. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/test_detector_types.py +0 -0
  207. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/test_phase2_detectors.py +0 -0
  208. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/test_registry.py +0 -0
  209. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/threat/__init__.py +0 -0
  210. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/threat/test_code_security_detector.py +0 -0
  211. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/detectors/threat/test_yara_detector.py +0 -0
  212. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  213. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/integration/test_wordpress_links_assets.py +0 -0
  214. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/pipeline/test_detector_outcomes.py +0 -0
  215. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/pipeline/test_detector_pipeline.py +0 -0
  216. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/pipeline/test_worker_pool.py +0 -0
  217. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_assets_metadata_catalog.py +0 -0
  218. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_azure_blob_storage_source.py +0 -0
  219. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_base_source_attachment.py +0 -0
  220. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_base_source_sampling.py +0 -0
  221. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_byte_valued_fields.py +0 -0
  222. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_config.py +0 -0
  223. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_confluence_source.py +0 -0
  224. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_custom_extractor.py +0 -0
  225. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_databricks_source.py +0 -0
  226. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_delta_lake_source.py +0 -0
  227. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_dependency_groups.py +0 -0
  228. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_dropbox_auth_command.py +0 -0
  229. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_dropbox_source.py +0 -0
  230. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_elasticsearch_source.py +0 -0
  231. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_email_source.py +0 -0
  232. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_file_evaluation_runner.py +0 -0
  233. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_google_cloud_storage_source.py +0 -0
  234. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_google_workspace_source.py +0 -0
  235. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_hashing.py +0 -0
  236. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_hive_source.py +0 -0
  237. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_hugging_face_source.py +0 -0
  238. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_iceberg_source.py +0 -0
  239. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_jira_source.py +0 -0
  240. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_kafka_source.py +0 -0
  241. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_local_folder_source.py +0 -0
  242. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_main.py +0 -0
  243. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_meilisearch_source.py +0 -0
  244. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_microsoft_365_source.py +0 -0
  245. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_mongodb_source.py +0 -0
  246. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_mssql_source.py +0 -0
  247. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_mysql_source.py +0 -0
  248. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_neo4j_source.py +0 -0
  249. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_notion_source.py +0 -0
  250. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_opensearch_source.py +0 -0
  251. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_oracle_source.py +0 -0
  252. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_outputs.py +0 -0
  253. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_postgresql_source.py +0 -0
  254. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_powerbi_source.py +0 -0
  255. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_recipe_normalizer.py +0 -0
  256. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_reddit_source.py +0 -0
  257. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_s3_compatible_storage_source.py +0 -0
  258. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_sampling_automatic.py +0 -0
  259. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_sandbox_source.py +0 -0
  260. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_scan_cache.py +0 -0
  261. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_servicedesk_source.py +0 -0
  262. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_slack_source.py +0 -0
  263. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_snowflake_source.py +0 -0
  264. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_source_dependency_groups.py +0 -0
  265. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_source_strategy_conformance.py +0 -0
  266. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_sqlite_source.py +0 -0
  267. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_tableau_source.py +0 -0
  268. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_tabular_automatic_sampling.py +0 -0
  269. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_tabular_utils.py +0 -0
  270. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_uv_sync.py +0 -0
  271. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_wordpress_source.py +0 -0
  272. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_youtube_source.py +0 -0
  273. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/test_youtube_source_integration.py +0 -0
  274. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/utils/test_archive_extraction.py +0 -0
  275. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/utils/test_content_extraction.py +0 -0
  276. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/utils/test_embedded_files.py +0 -0
  277. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/utils/test_file_metadata.py +0 -0
  278. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/utils/test_file_parser.py +0 -0
  279. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/utils/test_file_to_images.py +0 -0
  280. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/utils/test_legacy_office.py +0 -0
  281. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/utils/test_transcription.py +0 -0
  282. {classifyre_cli-0.4.82 → classifyre_cli-0.4.83}/tests/utils/test_video_processing.py +0 -0
@@ -0,0 +1,3 @@
1
+ $ uv sync
2
+ Resolved 307 packages in 237ms
3
+ Checked 51 packages in 6ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.82
3
+ Version: 0.4.83
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.82",
3
+ "version": "0.4.83",
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.82"
3
+ version = "0.4.83"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -977,13 +977,39 @@ class S3CompatibleStorageOptionalConnection(BaseModel):
977
977
  )
978
978
  max_object_bytes: int | None = Field(
979
979
  5242880,
980
- description='Maximum bytes downloaded per object for MIME detection and text extraction',
980
+ description='Maximum bytes of one object held in memory. Larger objects are streamed to a temporary file (or read by byte range where the provider supports it), so this bounds memory rather than the size of file that can be scanned. See max_file_bytes to refuse large objects outright.',
981
981
  ge=1024,
982
- le=52428800,
983
982
  )
984
983
  verify_ssl: bool | None = Field(
985
984
  True, description='TLS certificate verification toggle'
986
985
  )
986
+ max_file_bytes: int | None = Field(
987
+ None,
988
+ description='Refuse any object larger than this many bytes. 0 or unset means no limit: an object above max_object_bytes is spooled to disk rather than held in memory, so file size is bounded by free disk, not by RAM.',
989
+ ge=0,
990
+ )
991
+ max_archive_members: int | None = Field(
992
+ 200,
993
+ description='Maximum member files expanded from one archive into child assets. Bounds fan-out, and is the guard against a zip bomb.',
994
+ ge=1,
995
+ le=10000,
996
+ )
997
+ max_archive_member_bytes: int | None = Field(
998
+ 10485760,
999
+ description='Maximum uncompressed bytes read from a single archive member',
1000
+ ge=1024,
1001
+ )
1002
+ max_archive_total_bytes: int | None = Field(
1003
+ 104857600,
1004
+ description='Maximum uncompressed bytes read across all members of one archive. The decompression-ratio ceiling: a zip bomb hits this before it hits memory.',
1005
+ ge=1024,
1006
+ )
1007
+ max_embedded_files: int | None = Field(
1008
+ 200,
1009
+ description='Maximum embedded files (parquet image/audio columns, office media) expanded from one container into child assets per run',
1010
+ ge=1,
1011
+ le=10000,
1012
+ )
987
1013
 
988
1014
 
989
1015
  class S3CompatibleStorageOptional(BaseModel):
@@ -1129,9 +1155,35 @@ class AzureBlobStorageOptionalConnection(BaseModel):
1129
1155
  )
1130
1156
  max_object_bytes: int | None = Field(
1131
1157
  5242880,
1132
- description='Maximum bytes downloaded per blob for MIME detection and text extraction',
1158
+ description='Maximum bytes of one object held in memory. Larger objects are streamed to a temporary file (or read by byte range where the provider supports it), so this bounds memory rather than the size of file that can be scanned. See max_file_bytes to refuse large objects outright.',
1159
+ ge=1024,
1160
+ )
1161
+ max_file_bytes: int | None = Field(
1162
+ None,
1163
+ description='Refuse any object larger than this many bytes. 0 or unset means no limit: an object above max_object_bytes is spooled to disk rather than held in memory, so file size is bounded by free disk, not by RAM.',
1164
+ ge=0,
1165
+ )
1166
+ max_archive_members: int | None = Field(
1167
+ 200,
1168
+ description='Maximum member files expanded from one archive into child assets. Bounds fan-out, and is the guard against a zip bomb.',
1169
+ ge=1,
1170
+ le=10000,
1171
+ )
1172
+ max_archive_member_bytes: int | None = Field(
1173
+ 10485760,
1174
+ description='Maximum uncompressed bytes read from a single archive member',
1133
1175
  ge=1024,
1134
- le=52428800,
1176
+ )
1177
+ max_archive_total_bytes: int | None = Field(
1178
+ 104857600,
1179
+ description='Maximum uncompressed bytes read across all members of one archive. The decompression-ratio ceiling: a zip bomb hits this before it hits memory.',
1180
+ ge=1024,
1181
+ )
1182
+ max_embedded_files: int | None = Field(
1183
+ 200,
1184
+ description='Maximum embedded files (parquet image/audio columns, office media) expanded from one container into child assets per run',
1185
+ ge=1,
1186
+ le=10000,
1135
1187
  )
1136
1188
 
1137
1189
 
@@ -1185,9 +1237,35 @@ class GoogleCloudStorageOptionalConnection(BaseModel):
1185
1237
  )
1186
1238
  max_object_bytes: int | None = Field(
1187
1239
  5242880,
1188
- description='Maximum bytes downloaded per object for MIME detection and text extraction',
1240
+ description='Maximum bytes of one object held in memory. Larger objects are streamed to a temporary file (or read by byte range where the provider supports it), so this bounds memory rather than the size of file that can be scanned. See max_file_bytes to refuse large objects outright.',
1189
1241
  ge=1024,
1190
- le=52428800,
1242
+ )
1243
+ max_file_bytes: int | None = Field(
1244
+ None,
1245
+ description='Refuse any object larger than this many bytes. 0 or unset means no limit: an object above max_object_bytes is spooled to disk rather than held in memory, so file size is bounded by free disk, not by RAM.',
1246
+ ge=0,
1247
+ )
1248
+ max_archive_members: int | None = Field(
1249
+ 200,
1250
+ description='Maximum member files expanded from one archive into child assets. Bounds fan-out, and is the guard against a zip bomb.',
1251
+ ge=1,
1252
+ le=10000,
1253
+ )
1254
+ max_archive_member_bytes: int | None = Field(
1255
+ 10485760,
1256
+ description='Maximum uncompressed bytes read from a single archive member',
1257
+ ge=1024,
1258
+ )
1259
+ max_archive_total_bytes: int | None = Field(
1260
+ 104857600,
1261
+ description='Maximum uncompressed bytes read across all members of one archive. The decompression-ratio ceiling: a zip bomb hits this before it hits memory.',
1262
+ ge=1024,
1263
+ )
1264
+ max_embedded_files: int | None = Field(
1265
+ 200,
1266
+ description='Maximum embedded files (parquet image/audio columns, office media) expanded from one container into child assets per run',
1267
+ ge=1,
1268
+ le=10000,
1191
1269
  )
1192
1270
 
1193
1271
 
@@ -4712,9 +4790,8 @@ class DropboxOptionalConnection(BaseModel):
4712
4790
  )
4713
4791
  max_object_bytes: int | None = Field(
4714
4792
  5242880,
4715
- description='Maximum bytes downloaded per file for MIME detection and text extraction',
4793
+ description='Maximum bytes of one object held in memory. Larger objects are streamed to a temporary file (or read by byte range where the provider supports it), so this bounds memory rather than the size of file that can be scanned. See max_file_bytes to refuse large objects outright.',
4716
4794
  ge=1024,
4717
- le=52428800,
4718
4795
  )
4719
4796
  request_timeout_seconds: float | None = Field(
4720
4797
  60,
@@ -4728,6 +4805,33 @@ class DropboxOptionalConnection(BaseModel):
4728
4805
  ge=0,
4729
4806
  le=10,
4730
4807
  )
4808
+ max_file_bytes: int | None = Field(
4809
+ None,
4810
+ description='Refuse any object larger than this many bytes. 0 or unset means no limit: an object above max_object_bytes is spooled to disk rather than held in memory, so file size is bounded by free disk, not by RAM.',
4811
+ ge=0,
4812
+ )
4813
+ max_archive_members: int | None = Field(
4814
+ 200,
4815
+ description='Maximum member files expanded from one archive into child assets. Bounds fan-out, and is the guard against a zip bomb.',
4816
+ ge=1,
4817
+ le=10000,
4818
+ )
4819
+ max_archive_member_bytes: int | None = Field(
4820
+ 10485760,
4821
+ description='Maximum uncompressed bytes read from a single archive member',
4822
+ ge=1024,
4823
+ )
4824
+ max_archive_total_bytes: int | None = Field(
4825
+ 104857600,
4826
+ description='Maximum uncompressed bytes read across all members of one archive. The decompression-ratio ceiling: a zip bomb hits this before it hits memory.',
4827
+ ge=1024,
4828
+ )
4829
+ max_embedded_files: int | None = Field(
4830
+ 200,
4831
+ description='Maximum embedded files (parquet image/audio columns, office media) expanded from one container into child assets per run',
4832
+ ge=1,
4833
+ le=10000,
4834
+ )
4731
4835
 
4732
4836
 
4733
4837
  class DropboxOptionalTeam(BaseModel):
@@ -4892,9 +4996,8 @@ class HuggingFaceOptionalConnection(BaseModel):
4892
4996
  )
4893
4997
  max_object_bytes: int | None = Field(
4894
4998
  26214400,
4895
- description='Maximum bytes streamed per file for MIME detection and text extraction (default 25 MB). Larger files are read up to this cap and then the connection is dropped.',
4999
+ description='Maximum bytes of one object held in memory. Larger objects are streamed to a temporary file (or read by byte range where the provider supports it), so this bounds memory rather than the size of file that can be scanned. See max_file_bytes to refuse large objects outright.',
4896
5000
  ge=1024,
4897
- le=104857600,
4898
5001
  )
4899
5002
  request_timeout_seconds: float | None = Field(
4900
5003
  60,
@@ -4908,6 +5011,33 @@ class HuggingFaceOptionalConnection(BaseModel):
4908
5011
  ge=0,
4909
5012
  le=10,
4910
5013
  )
5014
+ max_file_bytes: int | None = Field(
5015
+ None,
5016
+ description='Refuse any object larger than this many bytes. 0 or unset means no limit: an object above max_object_bytes is spooled to disk rather than held in memory, so file size is bounded by free disk, not by RAM.',
5017
+ ge=0,
5018
+ )
5019
+ max_archive_members: int | None = Field(
5020
+ 200,
5021
+ description='Maximum member files expanded from one archive into child assets. Bounds fan-out, and is the guard against a zip bomb.',
5022
+ ge=1,
5023
+ le=10000,
5024
+ )
5025
+ max_archive_member_bytes: int | None = Field(
5026
+ 10485760,
5027
+ description='Maximum uncompressed bytes read from a single archive member',
5028
+ ge=1024,
5029
+ )
5030
+ max_archive_total_bytes: int | None = Field(
5031
+ 104857600,
5032
+ description='Maximum uncompressed bytes read across all members of one archive. The decompression-ratio ceiling: a zip bomb hits this before it hits memory.',
5033
+ ge=1024,
5034
+ )
5035
+ max_embedded_files: int | None = Field(
5036
+ 200,
5037
+ description='Maximum embedded files (parquet image/audio columns, office media) expanded from one container into child assets per run',
5038
+ ge=1,
5039
+ le=10000,
5040
+ )
4911
5041
 
4912
5042
 
4913
5043
  class HuggingFaceOptional(BaseModel):
@@ -114,8 +114,16 @@ class PayloadCursor:
114
114
 
115
115
  Returning None is the restart signal. It fires on an unreadable cursor,
116
116
  an unknown format version, a checksum that no longer matches the file,
117
- and a strategy switch in each case the stored offset points at rows we
118
- can no longer vouch for.
117
+ a strategy switch, and a completed sweep that covered no rows in each
118
+ case the stored offset points at rows we can no longer vouch for.
119
+
120
+ That last one is a repair. A cursor reading ``exhausted`` with an explicit
121
+ ``rows_seen: 0`` was written by a run that read nothing and concluded the
122
+ file was finished — the signature of a payload that failed to open, not
123
+ one that was scanned. Such a cursor retires the asset from every future
124
+ scan (the scan cache skips on ``exhausted``), so it is discarded on sight
125
+ and the sweep starts over. A genuinely empty payload is re-read instead
126
+ of skipped, which costs nothing.
119
127
  """
120
128
  if not isinstance(raw, dict):
121
129
  return None
@@ -137,6 +145,15 @@ class PayloadCursor:
137
145
 
138
146
  rows_seen = raw.get("rows_seen")
139
147
  offset = _coerce_int(raw.get("offset"), 0)
148
+ swept_nothing = (
149
+ isinstance(rows_seen, int) and not isinstance(rows_seen, bool) and rows_seen <= 0
150
+ )
151
+ if bool(raw.get("exhausted", False)) and swept_nothing:
152
+ logger.info(
153
+ "Discarding a payload cursor that claims a completed sweep of 0 rows; "
154
+ "the payload was never actually read, so this asset restarts its sweep"
155
+ )
156
+ return None
140
157
  return cls(
141
158
  offset=max(0, offset),
142
159
  rows_seen=(max(0, int(rows_seen)) if isinstance(rows_seen, int) else None),
@@ -227,6 +244,8 @@ class PayloadWindow:
227
244
  it for free (Parquet keeps it in the footer). RANDOM uses it to seek
228
245
  straight to a random window; without it, RANDOM falls back to reservoir
229
246
  sampling, which still visits every row but holds only one window.
247
+ AUTOMATIC uses it to decide when a sweep is genuinely complete instead of
248
+ inferring it from the size of the last window (see ``_advanced``).
230
249
  """
231
250
  take = max(1, self.rows_per_page)
232
251
  unit = max(1, rows_per_unit)
@@ -244,7 +263,7 @@ class PayloadWindow:
244
263
  yield from pages(0, None)
245
264
  return
246
265
 
247
- yield from self._iterate_automatic(pages, take, unit, on_cursor)
266
+ yield from self._iterate_automatic(pages, take, unit, on_cursor, row_count)
248
267
 
249
268
  # ── AUTOMATIC ────────────────────────────────────────────────────────
250
269
 
@@ -254,6 +273,7 @@ class PayloadWindow:
254
273
  take: int,
255
274
  unit: int,
256
275
  on_cursor: Callable[[PayloadCursor], None] | None,
276
+ row_count: int | None = None,
257
277
  ) -> Iterator[str]:
258
278
  start = self.start_row
259
279
  emitted = 0
@@ -276,15 +296,33 @@ class PayloadWindow:
276
296
  yield page
277
297
 
278
298
  if on_cursor is not None:
279
- on_cursor(self._advanced(start, emitted, take, unit))
299
+ on_cursor(self._advanced(start, emitted, take, unit, row_count))
280
300
 
281
- def _advanced(self, start: int, emitted: int, take: int, unit: int) -> PayloadCursor:
301
+ def _advanced(
302
+ self,
303
+ start: int,
304
+ emitted: int,
305
+ take: int,
306
+ unit: int,
307
+ row_count: int | None = None,
308
+ ) -> PayloadCursor:
282
309
  """The cursor to persist after emitting ``emitted`` pages from ``start``.
283
310
 
284
- An underfilled window means the reader ran off the end, which is what
285
- marks the pass complete: the offset wraps to 0 so the next run re-reads
286
- from the top, and ``exhausted`` tells the scan cache the payload has been
287
- covered.
311
+ ``exhausted`` is the flag that lets the scan cache skip this asset for
312
+ good, so it is only ever set on positive proof that every row has been
313
+ read:
314
+
315
+ * when the reader knows the payload's row count (Parquet keeps it in the
316
+ footer), the sweep is complete exactly when it has reached that count.
317
+ Nothing is inferred, which is what stops a file whose length is a
318
+ multiple of ``rows_per_page`` from wrapping forever;
319
+ * otherwise the only available signal is an underfilled window, and it is
320
+ trusted *except* when the window came back completely empty from row 0.
321
+ A reader that produced nothing at all has not proved the payload is
322
+ empty — it may have failed — and treating that as a finished sweep is
323
+ how an unreadable file gets banked as fully scanned and skipped for
324
+ ever after. Re-reading a genuinely empty payload costs one cheap pass;
325
+ the other mistake costs the whole file.
288
326
  """
289
327
  prior = self.prior
290
328
  passes = prior.passes if prior is not None else 0
@@ -292,10 +330,18 @@ class PayloadWindow:
292
330
  # rows each, so a full window is that many pages.
293
331
  expected_units = -(-take // unit)
294
332
  reached = start + emitted * unit
295
- if emitted < expected_units:
333
+
334
+ if row_count is not None and row_count >= 0:
335
+ complete = reached >= row_count
336
+ rows_seen = min(reached, row_count)
337
+ else:
338
+ complete = emitted < expected_units and not (emitted == 0 and start == 0)
339
+ rows_seen = reached
340
+
341
+ if complete:
296
342
  return PayloadCursor(
297
343
  offset=0,
298
- rows_seen=reached,
344
+ rows_seen=rows_seen,
299
345
  passes=passes + 1,
300
346
  exhausted=True,
301
347
  checksum=self.checksum,
@@ -303,7 +349,7 @@ class PayloadWindow:
303
349
  )
304
350
  return PayloadCursor(
305
351
  offset=reached,
306
- rows_seen=prior.rows_seen if prior is not None else None,
352
+ rows_seen=(rows_seen if row_count is not None else prior.rows_seen if prior else None),
307
353
  passes=passes,
308
354
  exhausted=False,
309
355
  checksum=self.checksum,
@@ -6,17 +6,22 @@ from typing import Any
6
6
  from urllib.parse import quote
7
7
 
8
8
  from ...models.generated_input import AzureBlobStorageInput
9
+ from ...utils.range_reader import CallableRangeReader, open_buffered
9
10
  from ..dependencies import require_module
10
11
  from ..object_storage.base import ObjectRef, ObjectStorageSourceBase
11
12
 
12
13
  logger = logging.getLogger(__name__)
13
14
 
15
+ _STREAM_CHUNK_BYTES = 1024 * 1024
16
+
14
17
 
15
18
  class AzureBlobStorageSource(ObjectStorageSourceBase):
16
19
  source_type = "azure_blob_storage"
17
20
  provider_label = "AZURE_BLOB_STORAGE"
18
21
  input_model = AzureBlobStorageInput
19
22
 
23
+ SUPPORTS_RANGE_READS = True
24
+
20
25
  def _required_container(self) -> str:
21
26
  container = str(self.config.required.container).strip()
22
27
  if not container:
@@ -109,6 +114,33 @@ class AzureBlobStorageSource(ObjectStorageSourceBase):
109
114
  content_type_hint=str(content_type_hint) if content_type_hint else None,
110
115
  )
111
116
 
117
+ def _blob_client(self, key: str) -> Any:
118
+ container_client = self._client().get_container_client(self._required_container())
119
+ return container_client.get_blob_client(key)
120
+
121
+ def _stream_object(self, ref: ObjectRef) -> Iterator[bytes]:
122
+ """Stream the whole blob in chunks — no cap, nothing held whole."""
123
+ downloader = self._blob_client(ref.key).download_blob(
124
+ timeout=self._request_timeout_seconds()
125
+ )
126
+ yield from downloader.chunks()
127
+
128
+ def _open_object_range_reader(self, ref: ObjectRef) -> Any | None:
129
+ """Azure takes offset/length, so a columnar blob needs no download."""
130
+ if not ref.size:
131
+ return None
132
+ blob_client = self._blob_client(ref.key)
133
+ timeout = self._request_timeout_seconds()
134
+ label = f"{self._required_container()}/{ref.key}"
135
+
136
+ def _fetch(start: int, end_inclusive: int) -> bytes:
137
+ downloader = blob_client.download_blob(
138
+ offset=start, length=end_inclusive - start + 1, timeout=timeout
139
+ )
140
+ return bytes(downloader.readall())
141
+
142
+ return open_buffered(CallableRangeReader(_fetch, size=int(ref.size), label=label))
143
+
112
144
  def _download_object(self, ref: ObjectRef) -> tuple[bytes, str | None]:
113
145
  blob_service_client = self._client()
114
146
  container = self._required_container()
@@ -378,7 +378,7 @@ class BaseSource(ABC):
378
378
 
379
379
  def iter_asset_pages(
380
380
  self,
381
- file_bytes: bytes,
381
+ file_bytes: bytes | Any,
382
382
  mime_type: str,
383
383
  batch_size: int = 100,
384
384
  include_column_names: bool = True,
@@ -397,6 +397,11 @@ class BaseSource(ABC):
397
397
 
398
398
  Everything else — PDFs, images, audio — has no row axis and streams
399
399
  unchanged.
400
+
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
403
+ decoded: a source that can serve byte ranges reads the footer and the one
404
+ row group this run needs out of an object it never downloads whole.
400
405
  """
401
406
  from ..utils.file_parser import count_tabular_rows, iter_file_pages
402
407
 
@@ -28,6 +28,8 @@ from ..object_storage.base import ObjectRef, ObjectStorageSourceBase
28
28
 
29
29
  logger = logging.getLogger(__name__)
30
30
 
31
+ _DOWNLOAD_CHUNK_BYTES = 1024 * 1024
32
+
31
33
  # Dropbox Paper docs (and a few other cloud-native items) carry no downloadable
32
34
  # bytes. They are exported instead; markdown keeps the text intact for detectors.
33
35
  _EXPORT_MIME_TYPES = {
@@ -587,6 +589,33 @@ class DropboxSource(ObjectStorageSourceBase):
587
589
 
588
590
  # ── download ─────────────────────────────────────────────────────────
589
591
 
592
+ def _stream_object(self, ref: ObjectRef) -> Iterator[bytes]:
593
+ """Stream a Dropbox file in chunks — no cap, nothing held whole.
594
+
595
+ Exported formats (Paper docs and friends) go through the same path: the
596
+ export is generated server-side and streamed back like any other file.
597
+ """
598
+ dropbox_ref = ref if isinstance(ref, DropboxObjectRef) else None
599
+ client = self._client_for(dropbox_ref.target if dropbox_ref else _ACCOUNT_TARGET)
600
+ path = dropbox_ref.file_id if dropbox_ref and dropbox_ref.file_id else ref.key
601
+
602
+ if dropbox_ref is not None and not dropbox_ref.is_downloadable:
603
+ export_format = dropbox_ref.export_format or "markdown"
604
+ _export_result, response = client.files_export(path, export_format)
605
+ self._exported_as_by_key[ref.key] = _EXPORT_MIME_TYPES.get(export_format, "text/plain")
606
+ else:
607
+ _metadata, response = client.files_download(path)
608
+
609
+ try:
610
+ yield from response.iter_content(chunk_size=_DOWNLOAD_CHUNK_BYTES)
611
+ finally:
612
+ close = getattr(response, "close", None)
613
+ if callable(close):
614
+ try:
615
+ close()
616
+ except Exception:
617
+ logger.debug("Failed to close Dropbox response body")
618
+
590
619
  def _download_object(self, ref: ObjectRef) -> tuple[bytes, str | None]:
591
620
  max_bytes = self._max_object_bytes()
592
621
  dropbox_ref = ref if isinstance(ref, DropboxObjectRef) else None
@@ -6,17 +6,22 @@ from collections.abc import Iterator
6
6
  from typing import Any
7
7
 
8
8
  from ...models.generated_input import GoogleCloudStorageInput
9
+ from ...utils.range_reader import CallableRangeReader, open_buffered
9
10
  from ..dependencies import require_module
10
11
  from ..object_storage.base import ObjectRef, ObjectStorageSourceBase
11
12
 
12
13
  logger = logging.getLogger(__name__)
13
14
 
15
+ _STREAM_CHUNK_BYTES = 1024 * 1024
16
+
14
17
 
15
18
  class GoogleCloudStorageSource(ObjectStorageSourceBase):
16
19
  source_type = "google_cloud_storage"
17
20
  provider_label = "GOOGLE_CLOUD_STORAGE"
18
21
  input_model = GoogleCloudStorageInput
19
22
 
23
+ SUPPORTS_RANGE_READS = True
24
+
20
25
  def _required_bucket(self) -> str:
21
26
  bucket = str(self.config.required.bucket).strip()
22
27
  if not bucket:
@@ -98,6 +103,31 @@ class GoogleCloudStorageSource(ObjectStorageSourceBase):
98
103
  content_type_hint=str(getattr(blob, "content_type", "") or "") or None,
99
104
  )
100
105
 
106
+ def _stream_object(self, ref: ObjectRef) -> Iterator[bytes]:
107
+ """Stream the whole blob in chunks — no cap, nothing held whole."""
108
+ blob = self._client().bucket(self._required_bucket()).blob(ref.key)
109
+ with blob.open("rb") as handle:
110
+ while True:
111
+ chunk = handle.read(_STREAM_CHUNK_BYTES)
112
+ if not chunk:
113
+ return
114
+ yield chunk
115
+
116
+ def _open_object_range_reader(self, ref: ObjectRef) -> Any | None:
117
+ """GCS takes an inclusive byte range, so a columnar blob needs no download."""
118
+ if not ref.size:
119
+ return None
120
+ bucket_name = self._required_bucket()
121
+ blob = self._client().bucket(bucket_name).blob(ref.key)
122
+ timeout = self._request_timeout_seconds()
123
+
124
+ def _fetch(start: int, end_inclusive: int) -> bytes:
125
+ return bytes(blob.download_as_bytes(start=start, end=end_inclusive, timeout=timeout))
126
+
127
+ return open_buffered(
128
+ CallableRangeReader(_fetch, size=int(ref.size), label=f"gs://{bucket_name}/{ref.key}")
129
+ )
130
+
101
131
  def _download_object(self, ref: ObjectRef) -> tuple[bytes, str | None]:
102
132
  client = self._client()
103
133
  bucket_name = self._required_bucket()
@@ -12,6 +12,7 @@ import requests
12
12
 
13
13
  from ...models.generated_input import HuggingFaceInput
14
14
  from ...models.generated_single_asset_scan_results import SingleAssetScanResults
15
+ from ...utils.range_reader import HttpRangeReader, open_buffered
15
16
  from ..dependencies import require_module
16
17
  from ..object_storage.base import ObjectRef, ObjectStorageSourceBase
17
18
 
@@ -59,6 +60,9 @@ class HuggingFaceSource(ObjectStorageSourceBase):
59
60
  provider_label = "HUGGING_FACE"
60
61
  input_model = HuggingFaceInput
61
62
 
63
+ # The Hub's resolve endpoint and its CDN both serve byte ranges.
64
+ SUPPORTS_RANGE_READS = True
65
+
62
66
  def __init__(
63
67
  self,
64
68
  recipe: dict[str, Any],
@@ -409,6 +413,66 @@ class HuggingFaceSource(ObjectStorageSourceBase):
409
413
  )
410
414
  return file_bytes, content_type
411
415
 
416
+ def _stream_object(self, ref: ObjectRef) -> Iterator[bytes]:
417
+ """Stream a Hub file in chunks — no cap, nothing held whole.
418
+
419
+ ``_open_object`` spools this, so the peak cost of a file the range reader
420
+ cannot help with (a PDF, a zip) is a temp file rather than its full size
421
+ in memory.
422
+ """
423
+ response = self._session().get(
424
+ self._resolve_url(ref.key),
425
+ headers={
426
+ "Authorization": f"Bearer {self._token()}",
427
+ "User-Agent": "classifyre",
428
+ },
429
+ stream=True,
430
+ timeout=self._request_timeout_seconds(),
431
+ allow_redirects=True,
432
+ )
433
+ response.raise_for_status()
434
+ try:
435
+ yield from response.iter_content(chunk_size=_DOWNLOAD_CHUNK_BYTES)
436
+ finally:
437
+ close = getattr(response, "close", None)
438
+ if callable(close):
439
+ try:
440
+ close()
441
+ except Exception:
442
+ logger.debug("Failed to close Hugging Face response body")
443
+
444
+ def _open_object_range_reader(self, ref: ObjectRef) -> Any | None:
445
+ """A seekable handle over a Hub file, served by HTTP range requests.
446
+
447
+ This is what makes a repository of multi-hundred-megabyte Parquet shards
448
+ scannable at all. ``max_object_bytes`` bounds a whole-file download, and
449
+ a Parquet file cut off at that bound has lost its footer — the index of
450
+ where the row groups are — so it yields not fewer rows but none. Reading
451
+ by range inverts the cost: the sampling window picks the rows, and only
452
+ the footer plus the row groups holding those rows ever cross the wire.
453
+
454
+ The Hub's ``resolve`` endpoint and its CDN both honour ``Range``. If a
455
+ deployment ever does not, ``HttpRangeReader`` still returns correct bytes
456
+ (it slices a full response) and says so in the log.
457
+ """
458
+ if not ref.size:
459
+ return None
460
+
461
+ reader = HttpRangeReader(
462
+ self._session(),
463
+ self._resolve_url(ref.key),
464
+ size=int(ref.size),
465
+ # As in _download_object: requests drops this on the redirect to the
466
+ # pre-signed CDN host, which is exactly what the Hub expects.
467
+ headers={
468
+ "Authorization": f"Bearer {self._token()}",
469
+ "User-Agent": "classifyre",
470
+ },
471
+ timeout=self._request_timeout_seconds(),
472
+ label=f"hugging_face:{ref.key}",
473
+ )
474
+ return open_buffered(reader)
475
+
412
476
  @staticmethod
413
477
  def _read_capped(response: Any, max_bytes: int) -> bytes:
414
478
  """Read at most ``max_bytes`` + 1 bytes, then drop the connection.
@@ -9,6 +9,7 @@ from typing import Any
9
9
  from urllib.parse import quote
10
10
 
11
11
  from ...models.generated_input import LocalFolderInput
12
+ from ...utils.file_parser import resolve_mime_type
12
13
  from ..object_storage.base import ObjectRef, ObjectStorageSourceBase
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -133,6 +134,27 @@ class LocalFolderSource(ObjectStorageSourceBase):
133
134
  def _list_objects(self) -> Iterator[ObjectRef]:
134
135
  yield from self._walk(self._root(), depth=0)
135
136
 
137
+ def _open_object(self, ref: ObjectRef) -> tuple[Any, str]:
138
+ """Open the file where it already is.
139
+
140
+ The one source that needs neither a download nor a spool: the payload is
141
+ a local file, so the parser reads it in place at any size. ``max_file_bytes``
142
+ keeps governing how much is pulled into memory by the *bytes* path
143
+ (binary detectors), not how large a file can be read.
144
+ """
145
+ file_path = self._root() / ref.key
146
+ handle = open(file_path, "rb")
147
+ try:
148
+ mime_type = resolve_mime_type(
149
+ handle,
150
+ declared_mime_type=ref.content_type_hint or "",
151
+ file_name=self._object_file_name(ref),
152
+ )
153
+ except Exception:
154
+ handle.close()
155
+ raise
156
+ return handle, mime_type
157
+
136
158
  def _download_object(self, ref: ObjectRef) -> tuple[bytes, str | None]:
137
159
  file_path = self._root() / ref.key
138
160
  max_bytes = self._max_file_bytes()